From a3963fe36ef86a5592689e3b3ca900e395cfcbc3 Mon Sep 17 00:00:00 2001 From: Tomasz Boruc Date: Sun, 11 Oct 2026 21:19:40 +0200 Subject: [PATCH] POI --- scripts/captain/captain_progress.gd | 1 + scripts/game/game.gd | 1 + scripts/game/game_state.gd | 2 + scripts/levels/celestial_body.gd | 2 +- scripts/levels/exploration_generator.gd | 54 ++++++ scripts/levels/exploration_generator.gd.uid | 1 + scripts/levels/exploration_poi.gd | 188 ++++++++++++++++++++ scripts/levels/exploration_poi.gd.uid | 1 + scripts/ui/ship_inventory_panel.gd | 6 + tests/levels/exploration_test.gd | 72 ++++++++ tests/levels/exploration_test.gd.uid | 1 + tests/levels/exploration_test.tscn | 5 + 12 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 scripts/levels/exploration_generator.gd create mode 100644 scripts/levels/exploration_generator.gd.uid create mode 100644 scripts/levels/exploration_poi.gd create mode 100644 scripts/levels/exploration_poi.gd.uid create mode 100644 tests/levels/exploration_test.gd create mode 100644 tests/levels/exploration_test.gd.uid create mode 100644 tests/levels/exploration_test.tscn diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd index 915e607..3d7e1e3 100644 --- a/scripts/captain/captain_progress.gd +++ b/scripts/captain/captain_progress.gd @@ -7,6 +7,7 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd") @export var experience: int = 0 @export var credits: int = 0 @export var parts: float = 0.0 +@export var minerals: Dictionary = {"titanium": 0, "iron": 0, "aluminium": 0} @export var learning_points: int = 0 @export var skills: Dictionary = { "piloting": 0, "gunner": 0, "mechanic": 0, "sharp_eye": 0, diff --git a/scripts/game/game.gd b/scripts/game/game.gd index 5730134..8cd334b 100644 --- a/scripts/game/game.gd +++ b/scripts/game/game.gd @@ -36,6 +36,7 @@ func _ready() -> void: camera.limit_bottom = int(WORLD_SIZE.y * 0.5) _spawn_celestial_bodies() _spawn_enemies() + preload("res://scripts/levels/exploration_generator.gd").spawn(self, system_data, player) var marker := Control.new() marker.set_script(PLANET_MARKER) marker.name = "ProtectedPlanetMarker" diff --git a/scripts/game/game_state.gd b/scripts/game/game_state.gd index 5377972..40298ac 100644 --- a/scripts/game/game_state.gd +++ b/scripts/game/game_state.gd @@ -7,6 +7,7 @@ var selected_ship_path: String = "" var restart_inventory: Dictionary = {} var system_level: int = 1 var system_seed: int = 0 +var explored_pois: Dictionary = {} func create_system_seed() -> int: var rng := RandomNumberGenerator.new() @@ -18,6 +19,7 @@ func start_new_run() -> void: system_level = 1 system_seed = create_system_seed() restart_inventory.clear() + explored_pois.clear() func _ready() -> void: var game_window := get_tree().root diff --git a/scripts/levels/celestial_body.gd b/scripts/levels/celestial_body.gd index 598be83..52aabbb 100644 --- a/scripts/levels/celestial_body.gd +++ b/scripts/levels/celestial_body.gd @@ -31,7 +31,7 @@ func _ready() -> void: _label = Label.new() _label.light_mask = 4 _label.position = Vector2(-160, radius + 14) - _label.size = Vector2(320, 54) + _label.size = Vector2(320, 80) _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _label.add_theme_font_size_override("font_size", 14) _update_label() diff --git a/scripts/levels/exploration_generator.gd b/scripts/levels/exploration_generator.gd new file mode 100644 index 0000000..7e3d035 --- /dev/null +++ b/scripts/levels/exploration_generator.gd @@ -0,0 +1,54 @@ +extends RefCounted + +const POI := preload("res://scripts/levels/exploration_poi.gd") + +static func spawn(game: Node2D, data: Dictionary, player: CharacterBody2D) -> void: + var rng := RandomNumberGenerator.new() + rng.seed = GameState.system_seed ^ 0x504f49 + var occupied: Array[Vector2] = [] + var wreck_count := rng.randi_range(2, 4) + for index in wreck_count + rng.randi_range(6, 10): + var kind := "wreck" if index < wreck_count else "asteroid" + var candidate := Vector2.ZERO + var found := false + for attempt in 200: + candidate = Vector2(rng.randf_range(-2300, 2300), rng.randf_range(-1500, 1500)) + if candidate.length() < 650 or candidate.distance_to(player.position) < 450: + continue + var clear := true + for planet in data["planets"]: + if candidate.distance_to(planet["position"]) < float(planet["radius"]) + 220: + clear = false + for point in occupied: + if candidate.distance_to(point) < 180: + clear = false + if clear: + found = true + break + if found: + occupied.append(candidate) + _add(game, player, kind, candidate, index, rng.randi()) + # Rare per system, at most one ruin on an uninhabited planet. + if rng.randf() < 0.08: + var candidates: Array[int] = [] + for index in data["planets"].size(): + if not data["planets"][index]["protected"]: + candidates.append(index) + if not candidates.is_empty(): + var index := candidates[rng.randi_range(0, candidates.size() - 1)] + var planet := game.get_node("CelestialBodies/Planet%d" % (index + 1)) + _add(game, player, "ruin", planet.position, 100, rng.randi(), float(planet.get("radius")) + 100.0) + +static func _add(game: Node2D, player: CharacterBody2D, kind: String, position: Vector2, index: int, reward_seed: int, approach_radius: float = 115.0) -> void: + var id := "%d:%d" % [GameState.system_seed, index] + if GameState.explored_pois.has(id): + return + var poi := StaticBody2D.new() + poi.set_script(POI) + poi.set("kind", kind) + poi.set("poi_id", id) + poi.set("reward_seed", reward_seed) + poi.set("player", player) + poi.set("approach_radius", approach_radius) + poi.position = position + game.add_child(poi) diff --git a/scripts/levels/exploration_generator.gd.uid b/scripts/levels/exploration_generator.gd.uid new file mode 100644 index 0000000..328dd87 --- /dev/null +++ b/scripts/levels/exploration_generator.gd.uid @@ -0,0 +1 @@ +uid://cx2fohii03ed6 diff --git a/scripts/levels/exploration_poi.gd b/scripts/levels/exploration_poi.gd new file mode 100644 index 0000000..18e3b35 --- /dev/null +++ b/scripts/levels/exploration_poi.gd @@ -0,0 +1,188 @@ +extends StaticBody2D + +const GENERATOR := preload("res://scripts/items/item_generator.gd") +const LOOT := preload("res://scripts/items/loot_pickup.gd") +var kind := "wreck" +var poi_id := "" +var reward_seed := 1 +var player: CharacterBody2D +var approach_radius := 115.0 +var hull := 20.0 +var _claimed := false +var _module_left_outside := false +var _label: Label +var _rng := RandomNumberGenerator.new() +var _mineral := "iron" +var _points := PackedVector2Array() + +func _ready() -> void: + add_to_group("exploration_pois") + _rng.seed = reward_seed + _mineral = ["iron", "aluminium", "titanium"][_rng.randi_range(0, 2)] + hull = 20.0 * reward_scale() + collision_layer = 4 if kind == "asteroid" else 0 + collision_mask = 0 + if kind == "asteroid": + var shape := CollisionShape2D.new() + var circle := CircleShape2D.new() + circle.radius = 30 + shape.shape = circle + add_child(shape) + for index in 10: + _points.append(Vector2.RIGHT.rotated(TAU * index / 10) * _rng.randf_range(23, 35)) + _label = Label.new() + _label.light_mask = 4 + _label.position = Vector2(-160, 40) + _label.size = Vector2(320, 70) + _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _label.add_theme_font_size_override("font_size", 12) + if kind == "ruin": + z_index = 20 + _label.position = Vector2(-40, -35) + _label.size = Vector2(80, 70) + _label.add_theme_font_size_override("font_size", 52) + _label.add_theme_color_override("font_color", Color("ffd166")) + _label.add_theme_color_override("font_outline_color", Color("151625")) + _label.add_theme_constant_override("outline_size", 8) + add_child(_label) + _update_label(false) + queue_redraw() + +func reward_scale() -> float: + return 1.0 + 0.05 * (clampi(GameState.system_level, 1, 1000) - 1) + +func _draw() -> void: + if kind == "asteroid": + draw_colored_polygon(_points, Color("655e56")) + var outline := _points.duplicate() + outline.append(_points[0]) + draw_polyline(outline, Color("a39a88"), 2, true) + draw_circle(Vector2(-8, -5), 7, Color("49443f")) + elif kind == "wreck": + draw_colored_polygon(PackedVector2Array([Vector2(-38, 20), Vector2(-16, -30), Vector2(7, -12), Vector2(22, 27), Vector2(-5, 14)]), Color("626b78")) + draw_line(Vector2(-12, -20), Vector2(4, 15), Color("df9c67"), 3, true) + draw_colored_polygon(PackedVector2Array([Vector2(28, -16), Vector2(42, -5), Vector2(29, 8)]), Color("434e60")) + +func _update_label(near: bool) -> void: + if kind == "ruin": + _label.text = "?" + elif kind == "asteroid": + _label.text = "Asteroida · %s\nHP: %.0f" % [{"iron": "Żelazo", "aluminium": "Aluminium", "titanium": "Tytan"}[_mineral], hull] + else: + _label.text = ("Wrak statku" if kind == "wreck" else "Antyczne znalezisko") + ("\n[E] Przeszukaj" if near else "\nPodleć, aby przeszukać") + +func _physics_process(_delta: float) -> void: + if _claimed or not is_instance_valid(player) or bool(player.get("_is_destroyed")): + return + var near := global_position.distance_to(player.global_position) <= approach_radius + _update_label(near) + if near and (kind == "ruin" or (kind == "wreck" and Input.is_physical_key_pressed(KEY_E))): + claim() + +func take_weapon_hit(amount: float, _critical: bool = false) -> void: + take_damage(amount) + +func take_damage(amount: float) -> void: + if kind != "asteroid" or _claimed: + return + hull -= maxf(0, amount) + if hull <= 0: + claim() + else: + _update_label(false) + +func claim() -> bool: + if _claimed or not is_instance_valid(player) or bool(player.get("_is_destroyed")): + return false + _claimed = true + GameState.explored_pois[poi_id] = true + var inventory := player.get_node("ShipInventory") as ShipInventory + var summary := "" + var scale := reward_scale() + if kind == "asteroid": + var amount := maxi(1, roundi(_rng.randi_range(1, 3) * scale)) + GameState.captain.minerals[_mineral] = int(GameState.captain.minerals.get(_mineral, 0)) + amount + summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu"}[_mineral]] + elif kind == "ruin": + var xp := roundi(_rng.randi_range(10, 20) * scale) + GameState.captain.add_rewards(xp, 0) + var module := _drop_module(true) + summary = "%d XP oraz %s (poziom %d)" % [xp, module.definition.display_name, module.item_level] + _show_discovery(summary) + else: + match _rng.randi_range(0, 3): + 0: + var amount := roundi(_rng.randi_range(3, 8) * scale) + GameState.captain.parts += amount + summary = "+%d części" % amount + 1: + _drop_module(false) + summary = "Znaleziono moduł" + 2: + var amount := maxi(1, roundi(scale)) + inventory.missile_ammunition += amount + summary = "+%d rakiet" % amount + 3: + var amount := roundi(_rng.randi_range(5, 15) * scale) + inventory.ammunition += amount + summary = "+%d amunicji" % amount + inventory.ammunition_changed.emit() + GameState.captain.emit_changed() + if kind != "ruin": + var popup := preload("res://scenes/game/damage_popup.tscn").instantiate() as Label + popup.text = summary + popup.set("text_color", Color("c9e8ff")) + popup.position = position + Vector2(-80, -65) + get_parent().add_child.call_deferred(popup) + set_physics_process(false) + queue_free() + return true + +func _show_discovery(reward: String) -> void: + var layer := CanvasLayer.new() + layer.layer = 30 + layer.name = "AncientDiscoveryMessage" + get_parent().add_child(layer) + var panel := PanelContainer.new() + panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + layer.add_child(panel) + panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) + panel.offset_left = -300 + panel.offset_right = 300 + panel.offset_top = 70 + var label := Label.new() + label.custom_minimum_size = Vector2(600, 95) + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + label.mouse_filter = Control.MOUSE_FILTER_IGNORE + label.add_theme_font_size_override("font_size", 18) + label.add_theme_color_override("font_color", Color("ffd166")) + label.text = "Znalazłeś ślady starożytnej upadłej cywilizacji!\nZdobyłeś: " + reward + if _module_left_outside: + label.text += "\nMagazyn pełny — moduł czeka obok planety." + panel.add_child(label) + var tween := layer.create_tween() + tween.tween_interval(7.0) + tween.tween_property(panel, "modulate:a", 0.0, 0.5) + tween.tween_callback(layer.queue_free) + +func _drop_module(ancient: bool) -> ItemInstance: + var definitions: Array[ItemDefinition] = [] + for definition in GENERATOR.DEFINITIONS: + if definition.category != ItemDefinition.Category.WEAPON: + definitions.append(definition) + var item := GENERATOR.generate_for_definition(definitions[_rng.randi_range(0, definitions.size() - 1)], mini(1000, GameState.system_level + (1 if ancient else 0)), 2 if ancient else 0) + if ancient: + for modifier in item.additional_modifiers: + var bounds := modifier.affix.get_value_range(item.item_level) + modifier.value = lerpf(bounds.x, bounds.y, _rng.randf_range(0.9, 1.0)) + if player.get_node("ShipInventory").add_item(item): + return item + _module_left_outside = true + var loot := Node2D.new() + loot.set_script(LOOT) + loot.set("item", item) + loot.position = position + Vector2(0, 70) + get_parent().add_child.call_deferred(loot) + return item diff --git a/scripts/levels/exploration_poi.gd.uid b/scripts/levels/exploration_poi.gd.uid new file mode 100644 index 0000000..fc3f578 --- /dev/null +++ b/scripts/levels/exploration_poi.gd.uid @@ -0,0 +1 @@ +uid://dmmyfie4rijcn diff --git a/scripts/ui/ship_inventory_panel.gd b/scripts/ui/ship_inventory_panel.gd index 53fbef0..2bc0587 100644 --- a/scripts/ui/ship_inventory_panel.gd +++ b/scripts/ui/ship_inventory_panel.gd @@ -13,6 +13,7 @@ var _inventory: ShipInventory var _drawer_open := false var _slide_tween: Tween var _missile_button: Button +var _minerals_label: Label var drawer_width := 780.0 func _ready() -> void: @@ -29,6 +30,10 @@ func _ready() -> void: _missile_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL _missile_button.pressed.connect(func() -> void: _inventory.craft_missile()) missile_row.add_child(_missile_button) + _minerals_label = Label.new() + _minerals_label.add_theme_font_size_override("font_size", 12) + $Contents.add_child(_minerals_label) + $Contents.move_child(_minerals_label, $Contents/CargoGrid.get_index()) _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory if _inventory: _inventory.inventory_changed.connect(_refresh_title) @@ -42,6 +47,7 @@ func _ready() -> void: _refresh_title() func _refresh_title() -> void: + _minerals_label.text = "SUROWCE · Tytan: %d · Żelazo: %d · Aluminium: %d" % [GameState.captain.minerals.get("titanium", 0), GameState.captain.minerals.get("iron", 0), GameState.captain.minerals.get("aluminium", 0)] craft_button.disabled = _inventory == null or not _inventory.can_craft_ammunition() _missile_button.disabled = _inventory == null or not _inventory.can_craft_missile() if _inventory: diff --git a/tests/levels/exploration_test.gd b/tests/levels/exploration_test.gd new file mode 100644 index 0000000..10bb548 --- /dev/null +++ b/tests/levels/exploration_test.gd @@ -0,0 +1,72 @@ +extends Node2D +const POI = preload("res://scripts/levels/exploration_poi.gd") +func _ready() -> void: + GameState.start_new_run() + var ship = preload("res://scenes/game/player_ship.tscn").instantiate() + ship.name = "PlayerShip" + add_child(ship) + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + var inv = ship.get_node("ShipInventory") + for kind in ["wreck", "asteroid", "ruin"]: + for index in 12: + var poi = StaticBody2D.new() + poi.set_script(POI) + poi.kind = kind + poi.player = ship + poi.reward_seed = index + 1 + poi.poi_id = kind + str(index) + add_child(poi) + if kind == "asteroid": + poi.take_weapon_hit(poi.hull - 1) + assert(not poi._claimed and is_equal_approx(poi.hull, 1)) + poi.take_weapon_hit(1) + assert(poi._claimed) + else: + assert(poi.claim()) + assert(not poi.claim()) + await get_tree().process_frame + assert(GameState.captain.parts > 0) + assert(inv.ammunition > 50 and inv.missile_ammunition > 2) + assert(GameState.captain.minerals.iron + GameState.captain.minerals.titanium + GameState.captain.minerals.aluminium > 0) + var ancient = 0 + var modules: Array = inv.cargo.filter(func(item): return item != null) + for loot in get_tree().get_nodes_in_group("loot"): + modules.append(loot.item) + for item in modules: + if item.item_level == 2: + ancient += 1 + assert(item.get_addon_count() >= 4) + for modifier in item.additional_modifiers: + var bounds = modifier.affix.get_value_range(2) + assert(modifier.value >= lerpf(bounds.x, bounds.y, 0.9) - 0.00001) + assert(ancient == 12) + var discovery = StaticBody2D.new() + discovery.set_script(POI) + discovery.kind = "ruin" + discovery.player = ship + discovery.poi_id = "auto-discovery" + discovery.position = Vector2(1000, 0) + discovery.approach_radius = 200 + add_child(discovery) + assert(discovery._label.text == "?") + discovery._physics_process(0) + assert(not discovery._claimed) + ship.position = Vector2(810, 0) + discovery._physics_process(0) + assert(discovery._claimed and GameState.explored_pois.has("auto-discovery")) + assert(not discovery.claim()) + var data = preload("res://scripts/levels/system_generator.gd").generate(1, 1234) + var bodies = Node2D.new() + bodies.name = "CelestialBodies" + add_child(bodies) + preload("res://scripts/levels/exploration_generator.gd").spawn(self, data, ship) + var generated = get_tree().get_nodes_in_group("exploration_pois").filter(func(p): return not p.is_queued_for_deletion()) + assert(generated.size() >= 8 and generated.size() <= 14) + var probe = generated[0] + assert(probe.reward_scale() == 1.0) + GameState.system_level = 10 + assert(is_equal_approx(probe.reward_scale(), 1.45)) + GameState.system_level = 1 + print("EXPLORATION_TEST: PASS rewards, duplicate prevention, minerals, ancient level+1 and high addons, generation") + get_tree().quit() diff --git a/tests/levels/exploration_test.gd.uid b/tests/levels/exploration_test.gd.uid new file mode 100644 index 0000000..045c837 --- /dev/null +++ b/tests/levels/exploration_test.gd.uid @@ -0,0 +1 @@ +uid://bwrnlc3vjc2ty diff --git a/tests/levels/exploration_test.tscn b/tests/levels/exploration_test.tscn new file mode 100644 index 0000000..b9104cd --- /dev/null +++ b/tests/levels/exploration_test.tscn @@ -0,0 +1,5 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/levels/exploration_test.gd" id="1"] +[node name="Test" type="Node2D"] +script = ExtResource("1") +