From 0f7edff15a01e0da1f99c80df5d2ff7431579177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Thu, 3 Sep 2026 00:12:08 +0200 Subject: [PATCH] Complete lab, quest, garrison, and energy tickets --- buildings/GasBurnerBuildingDefinition.tres | 2 +- controllers/BuildController.gd | 17 ++++++--- core/CombatTarget.gd | 26 ++++++++++++++ core/CombatTarget.gd.uid | 1 + core/Quests.gd | 8 +++++ enemies/Enemy.gd | 10 +++--- tests/_lab_menu_preview.gd | 15 ++++++-- tests/test_dialog_releases_joysticks.gd.uid | 1 + tests/test_gas_burner.gd | 8 +++-- tests/test_gas_burner_pause.gd | 4 +-- tests/test_outpost_garrison_patrol.gd | 33 +++++++++++++++-- tests/test_quest_chain.gd | 17 +++++++++ tests/test_ship_lab.gd | 31 ++++++++++++++++ ui/menus/LabMenu.gd | 11 ++++++ ui/menus/LabMenu.tscn | 40 +++++++++++++++++++-- vessels/Projectile.gd | 8 +++-- vessels/Ship.gd | 16 ++++----- vessels/VesselGarrisonComponent.gd | 27 +++++++++----- vessels/VesselWeaponComponent.gd | 4 +-- world/Asteroid.gd | 4 ++- 20 files changed, 238 insertions(+), 45 deletions(-) create mode 100644 core/CombatTarget.gd create mode 100644 core/CombatTarget.gd.uid create mode 100644 tests/test_dialog_releases_joysticks.gd.uid diff --git a/buildings/GasBurnerBuildingDefinition.tres b/buildings/GasBurnerBuildingDefinition.tres index 59589aa..2615af7 100644 --- a/buildings/GasBurnerBuildingDefinition.tres +++ b/buildings/GasBurnerBuildingDefinition.tres @@ -12,7 +12,7 @@ input_buffers = { collected_outputs = PackedStringArray("energy") conversion_input = "gas" conversion_output = "energy" -conversion_rate = 1.0 +conversion_rate = 0.2 conversion_yield = 2 [resource] diff --git a/controllers/BuildController.gd b/controllers/BuildController.gd index c9589d8..7fa56fd 100644 --- a/controllers/BuildController.gd +++ b/controllers/BuildController.gd @@ -143,6 +143,8 @@ func _process(delta: float) -> void: build_menu.refresh_option_states(_planet_under_fleet()) if craft_menu.visible: craft_menu.refresh() + if lab_menu.visible: + _refresh_lab() if transfer_menu.visible: transfer_menu.refresh() if world_map.visible or _menu_is_open(): @@ -592,6 +594,8 @@ func _on_lab_buy() -> void: func _refresh_lab() -> void: + lab_menu.set_status("") + lab_menu.hide_research_progress() var definitions: Array = VESSEL_DEFINITIONS.all() lab_menu.render_vessels(definitions, lab_vessel) lab_menu.vessel_heading.text = "%s research ยท %d RP" % [ @@ -622,13 +626,16 @@ func _refresh_lab() -> void: or not GameState.upgrade_maxed(selected.id)) if not queued.is_empty(): var queued_points: int = GameState.research_point_cost(String(queued.id)) + var progress: float = float(queued.elapsed) \ + / maxf(GameState.lab_research_time(), 0.001) if not bool(queued.started) and GameState.research_points < queued_points: - lab_menu.set_status("Need %d research point%s (have %d)" % [queued_points, - "" if queued_points == 1 else "s", GameState.research_points]) + lab_menu.show_research_progress( + "Need %d research point%s (have %d)" % [queued_points, + "" if queued_points == 1 else "s", GameState.research_points], progress) else: - lab_menu.set_status("%s %d%%" % ["Researching" if bool(queued.started) - else "Waiting for resources", int(100.0 * float(queued.elapsed) - / maxf(GameState.lab_research_time(), 0.001))]) + lab_menu.show_research_progress( + "Researching" if bool(queued.started) else "Waiting for resources", + progress) func _on_transfer_pressed() -> void: diff --git a/core/CombatTarget.gd b/core/CombatTarget.gd new file mode 100644 index 0000000..72a0b3e --- /dev/null +++ b/core/CombatTarget.gd @@ -0,0 +1,26 @@ +class_name CombatTarget +extends RefCounted + +# One opt-in contract for anything a player weapon may deliberately attack. +# Aim assist, autonomous garrisons and projectile impacts all consume the same +# group, so a new target composition does not need a concrete-type branch in +# each combat system. +const GROUP: StringName = &"player_weapon_targets" +const PRIORITY_META: StringName = &"player_weapon_target_priority" + +# A garrison protects its post before mining nearby debris. Distances still +# decide between targets at the same priority. +const RESOURCE_PRIORITY: int = 0 +const DANGER_PRIORITY: int = 100 + + +static func register(node: Node, priority: int = RESOURCE_PRIORITY) -> void: + if node == null: + return + node.add_to_group(GROUP) + node.set_meta(PRIORITY_META, priority) + + +static func priority(node: Node) -> int: + return int(node.get_meta(PRIORITY_META, RESOURCE_PRIORITY)) if node != null \ + else RESOURCE_PRIORITY diff --git a/core/CombatTarget.gd.uid b/core/CombatTarget.gd.uid new file mode 100644 index 0000000..40c5552 --- /dev/null +++ b/core/CombatTarget.gd.uid @@ -0,0 +1 @@ +uid://pgp5qd13xgro diff --git a/core/Quests.gd b/core/Quests.gd index 16c12f9..78ba22d 100644 --- a/core/Quests.gd +++ b/core/Quests.gd @@ -242,6 +242,14 @@ const CHAIN: Array = [ "done": "Three kinds of world surveyed. Not one of them holds any sign of the convoy.", "important": true, }, + { + "id": "propulser", + "title": "A LONGER REACH", + "objective": "Build an Energy Propulser vessel", + "goal": {"kind": GOAL_FLEET_VESSEL, "vessel": "distorder", "count": 1}, + "brief": "The trail may run far beyond this cluster. Unlock an Energy Propulser at the ship lab, build one at the yard, and crew it. Its drive will let the whole fleet search faster.", + "done": "The Energy Propulser is in formation. We can cast the search wider now.", + }, { "id": "trail", "title": "THE TRAIL", diff --git a/enemies/Enemy.gd b/enemies/Enemy.gd index d4bec9a..6f64c3b 100644 --- a/enemies/Enemy.gd +++ b/enemies/Enemy.gd @@ -1,5 +1,7 @@ extends Area2D +const COMBAT_TARGET = preload("res://core/CombatTarget.gd") + signal destroyed const ENEMY_DEFINITIONS = preload("res://enemies/EnemyDefinitions.gd") @@ -30,10 +32,10 @@ var max_hp: int func _ready() -> void: add_to_group("enemies") - # Weapons discover anything the player may deliberately aim at through one - # compositional contract. Asteroids implement the same contract without the - # aim-assist code having to know either concrete type (#181). - add_to_group("player_aim_targets") + # Weapons discover anything the player may deliberately attack through one + # compositional contract. Asteroids implement the same contract without + # combat systems having to know either concrete type (#181, #186). + COMBAT_TARGET.register(self, COMBAT_TARGET.DANGER_PRIORITY) definition = ENEMY_DEFINITIONS.get_definition(enemy_id) speed = definition.speed acceleration = definition.acceleration diff --git a/tests/_lab_menu_preview.gd b/tests/_lab_menu_preview.gd index 7395fe8..1fc6442 100644 --- a/tests/_lab_menu_preview.gd +++ b/tests/_lab_menu_preview.gd @@ -1,7 +1,7 @@ extends Node2D # Preview, not a test - the ship lab at the real 720x1280 viewport, with a -# vessel selected and its upgrades listed. +# vessel selected, its upgrades listed, and active research in progress. # # bash tests/screenshot.sh res://tests/_lab_menu_preview.tscn /tmp/lab.png 25 720x1280 @@ -13,10 +13,21 @@ func _ready() -> void: GameState.bank_inventory.set_amount(GameState.RESOURCE_ORE, 310) GameState.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 47) GameState.research_points = 4 + GameState.build_time_scale = 1.0 + var lab_id: int = GameState.add_building(GameState.BUILDING_SHIP_LAB, + Vector2.ZERO) + GameState.queue_lab_research(lab_id, "int_rate") + GameState.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE, + GameState.upgrade_cost("int_rate")) + GameState.call("_advance_lab_research", GameState.lab_research_time() * 0.42) + # Freeze the model so a slow software-rendered capture cannot finish the + # research before it takes the frame this preview exists to show. + GameState.set_process(false) var main: Node = load("res://world/Main.tscn").instantiate() add_child(main) main.get_node("EnemySpawner").set_process(false) main.get_node("AsteroidField").set_process(false) var build: Node = main.get_node("BuildController") - build.lab_menu.visible = true + build.lab_building_id = lab_id + build.lab_menu.open_for(lab_id) build._refresh_lab() diff --git a/tests/test_dialog_releases_joysticks.gd.uid b/tests/test_dialog_releases_joysticks.gd.uid new file mode 100644 index 0000000..e7268c5 --- /dev/null +++ b/tests/test_dialog_releases_joysticks.gd.uid @@ -0,0 +1 @@ +uid://qgrnenhp5sqh diff --git a/tests/test_gas_burner.gd b/tests/test_gas_burner.gd index 4b97dc5..b753285 100644 --- a/tests/test_gas_burner.gd +++ b/tests/test_gas_burner.gd @@ -53,6 +53,8 @@ func _initialize() -> void: var logistics: Resource = game_state.building_logistics_profile(game_state.find_building(burner_id)) conversion_rate = logistics.conversion_rate conversion_yield = logistics.conversion_yield + if not is_equal_approx(conversion_rate, 0.2): + _fail("burner conversion rate is %.2f/s, expected the reduced 0.20/s" % conversion_rate) func _process(delta: float) -> bool: @@ -72,13 +74,13 @@ func _process(delta: float) -> bool: print("energy is generic: in resource_types(), own definition, own colour %s" % energy_color) # --- B. produces energy from local gas at the configured rate - - if elapsed < 2.5: + if elapsed < 5.5: return false var gas_left: int = game_state.building_resource(burner_id, GameState.RESOURCE_GAS) var energy_made: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY) var gas_burned: int = 20 - gas_left - if gas_burned < 2: - return _fail("burner only burned %d gas in 2.5s at rate %.1f/s" % [gas_burned, conversion_rate]) + if gas_burned < 1: + return _fail("burner burned no gas in 5.5s at rate %.2f/s" % conversion_rate) if energy_made != gas_burned * conversion_yield: return _fail("burner made %d energy for %d gas burned, expected %dx" % [ energy_made, gas_burned, conversion_yield diff --git a/tests/test_gas_burner_pause.gd b/tests/test_gas_burner_pause.gd index 50471c7..9833d86 100644 --- a/tests/test_gas_burner_pause.gd +++ b/tests/test_gas_burner_pause.gd @@ -84,7 +84,7 @@ func _process(delta: float) -> bool: return false 1: # --- B. running, it converts ---------------------------------- - if elapsed < 1.5: + if elapsed < 5.5: return false if _stock(GameState.RESOURCE_ENERGY) <= 0: return _fail("a running burner made no energy in %.1fs" % elapsed) @@ -119,7 +119,7 @@ func _process(delta: float) -> bool: elapsed = 0.0 return false 3: - if elapsed < 1.5: + if elapsed < 5.5: return false if _stock(GameState.RESOURCE_ENERGY) <= energy_at_pause: return _fail("a resumed burner made no further energy") diff --git a/tests/test_outpost_garrison_patrol.gd b/tests/test_outpost_garrison_patrol.gd index 225709d..f064d62 100644 --- a/tests/test_outpost_garrison_patrol.gd +++ b/tests/test_outpost_garrison_patrol.gd @@ -21,6 +21,8 @@ extends SceneTree # E. it goes back to patrolling once the enemy is gone # F. losing a garrison vessel frees the berth, so it gets crewed again # G. enemies fight back - a garrison vessel is a target like any other +# H. #186 - with no hostile nearby, the same weapon-target contract lets a +# garrison shoot asteroids for ore var game_state: Node var main: Node @@ -30,6 +32,7 @@ var outpost_id: int = 0 var guard: Node2D var component: Node var enemy: Node2D +var asteroid: Node2D var stage: int = 0 var elapsed: float = 0.0 var orbit_min: float = INF @@ -231,14 +234,40 @@ func _process(delta: float) -> bool: return _fail("the garrison is still fighting an enemy that is gone") print("stood down and went back to its patrol") + # --- H. asteroids are valid lower-priority targets ----------- + asteroid = load("res://world/Asteroid.tscn").instantiate() + asteroid.position = Vector2(300.0, 0.0) + asteroid.velocity = Vector2.ZERO + main.add_child(asteroid) + asteroid.hp = 100000 + dummy_hp_before = asteroid.hp + elapsed = 0.0 + stage = 6 + return false + 6: + if elapsed < 2.5: + return false + if not component.is_engaging(): + return _fail("an asteroid beside the outpost was not engaged") + if asteroid.hp >= dummy_hp_before: + return _fail("the garrison engaged an asteroid but never shot it") + print("garrison shot a nearby asteroid through the shared target contract") + asteroid.queue_free() + elapsed = 0.0 + stage = 7 + return false + 7: + if elapsed < 0.5: + return false + # --- F. a lost vessel frees its berth ------------------------ if game_state.garrison_crew(outpost_id).size() != 1: return _fail("the crew is not 1 before the kill") guard.take_damage(1) elapsed = 0.0 - stage = 6 + stage = 8 return false - 6: + 8: if elapsed < 0.3: return false if not game_state.garrison_crew(outpost_id).is_empty(): diff --git a/tests/test_quest_chain.gd b/tests/test_quest_chain.gd index e764373..66f3807 100644 --- a/tests/test_quest_chain.gd +++ b/tests/test_quest_chain.gd @@ -133,6 +133,23 @@ func _catalogue_cases() -> bool: return _fail("the quest after the mothership is %s, not exploration" % after.id) print("the mothership answers with distance, and exploration follows it") + # #185: the final wide search has a propulsion lesson immediately before + # it. The goal uses the stable vessel id while the player-facing copy uses + # the renamed Energy Propulser name. + var trail: Dictionary = QUESTS.by_id("trail") + var trail_at: int = QUESTS.CHAIN.find(trail) + if trail_at <= 0: + return _fail("the Chart 150 quest is missing or opens the chain") + var before_trail: Dictionary = QUESTS.at(trail_at - 1) + if String(before_trail.id) != "propulser" \ + or String(before_trail.goal.kind) != QUESTS.GOAL_FLEET_VESSEL \ + or String(before_trail.goal.vessel) != GameState.VESSEL_DISTORDER: + return _fail("Chart 150 is preceded by %s, not the Energy Propulser vessel quest" \ + % before_trail.get("id", "nothing")) + if not "Energy Propulser" in String(before_trail.objective): + return _fail("the propulsion quest does not name the Energy Propulser to the player") + print("the Energy Propulser vessel quest leads directly into Chart 150") + # --- R. #145: every structure quest can actually be finished --------- # The extractor quest could not be. Extractors are placed onto a planet # from space and live in their own array, not in `buildings`, so the diff --git a/tests/test_ship_lab.gd b/tests/test_ship_lab.gd index 560e55d..80e5577 100644 --- a/tests/test_ship_lab.gd +++ b/tests/test_ship_lab.gd @@ -25,6 +25,7 @@ var scene: Node var fleet: Node var field: Node var controller: Node +var lab_id: int = 0 var crafter_id: int = 0 var stage: int = 0 var elapsed: float = 0.0 @@ -83,6 +84,7 @@ func _process(delta: float) -> bool: game_state.unlocked = ["shipyards", "vessel_science", "coordination", "wing", "squadron"] game_state.research_points = 10 field.place(GameState.BUILDING_SHIP_LAB, Vector2(-700.0, 0.0)) + lab_id = int(game_state.buildings[-1].id) field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(700.0, 0.0)) crafter_id = int(game_state.buildings[-1].id) elapsed = 0.0 @@ -130,6 +132,29 @@ func _process(delta: float) -> bool: return true print("lab cards are vessel-specific and selectable") + # #184: a queued research uses the bar as its changing value. The + # status names the state but never repeats it as a percentage. + var lab_progress: ProgressBar = controller.get_node( + "../UI/LabMenu/ResearchProgress") + var lab_status: Label = controller.get_node("../UI/LabMenu/StatusLabel") + controller.lab_building_id = lab_id + if not game_state.queue_lab_research(lab_id, "int_rate"): + return _fail("could not queue research for the progress-bar check") + controller.call("_refresh_lab") + if not lab_progress.visible or lab_progress.show_percentage: + return _fail("pending research did not show a percentage-free progress bar") + if "%" in lab_status.text: + return _fail("pending research still prints a percentage: '%s'" % lab_status.text) + game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE, + game_state.upgrade_cost("int_rate")) + game_state.call("_advance_lab_research", game_state.lab_research_time() * 0.25) + controller.call("_refresh_lab") + if lab_progress.value < 0.2 or lab_progress.value > 0.3 \ + or lab_status.text != "Researching": + return _fail("research bar did not track active work (value %.2f, status '%s')" \ + % [lab_progress.value, lab_status.text]) + print("pending lab research is shown by a live progress bar, without percentage text") + # --- 3. buying raises the level and charges ------------------ var before_rocks: int = game_state.rocks var first_cost: int = game_state.upgrade_cost("int_damage") @@ -225,3 +250,9 @@ func _process(delta: float) -> bool: quit(0) return true return false + + +func _fail(message: String) -> bool: + print("TEST_SHIP_LAB: FAIL (%s)" % message) + quit(1) + return true diff --git a/ui/menus/LabMenu.gd b/ui/menus/LabMenu.gd index 9366f38..e6834d1 100644 --- a/ui/menus/LabMenu.gd +++ b/ui/menus/LabMenu.gd @@ -22,6 +22,7 @@ const VESSEL_MARGIN := 24.0 @onready var vessel_heading: Label = $VesselLabel @onready var detail: Label = $DetailLabel @onready var status: Label = $StatusLabel +@onready var research_progress: ProgressBar = $ResearchProgress @onready var resource_bar: Control = $Bar var building_id: int = 0 @@ -96,6 +97,16 @@ func set_status(message: String) -> void: status.text = message +func show_research_progress(message: String, ratio: float) -> void: + status.text = message + research_progress.value = clampf(ratio, 0.0, 1.0) + research_progress.visible = true + + +func hide_research_progress() -> void: + research_progress.visible = false + + func _build_price_tags() -> void: for i in range(upgrade_labels.size()): var label: Control = upgrade_labels[i] diff --git a/ui/menus/LabMenu.tscn b/ui/menus/LabMenu.tscn index adf32c5..d284d82 100644 --- a/ui/menus/LabMenu.tscn +++ b/ui/menus/LabMenu.tscn @@ -1,9 +1,28 @@ -[gd_scene load_steps=4 format=3] +[gd_scene load_steps=6 format=3] [ext_resource type="Script" path="res://ui/IconButton.gd" id="10"] [ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"] [ext_resource type="Script" path="res://ui/menus/LabMenu.gd" id="menu_script"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progress_background"] +bg_color = Color(0.025, 0.045, 0.09, 0.92) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.18, 0.48, 0.62, 0.9) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progress_fill"] +bg_color = Color(0.3, 0.9, 1, 0.88) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 + [node name="LabMenu" type="Control" groups=["modal_menus"]] visible = false mouse_filter = 2 @@ -283,12 +302,29 @@ anchor_bottom = 1.0 offset_left = 32 offset_top = -174 offset_right = -32 -offset_bottom = -120 +offset_bottom = -128 theme_override_font_sizes/font_size = 30 autowrap_mode = 2 horizontal_alignment = 1 text = "" +[node name="ResearchProgress" type="ProgressBar" parent="."] +visible = false +mouse_filter = 2 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -280 +offset_top = -116 +offset_right = 280 +offset_bottom = -82 +theme_override_styles/background = SubResource("StyleBoxFlat_progress_background") +theme_override_styles/fill = SubResource("StyleBoxFlat_progress_fill") +max_value = 1.0 +step = 0.001 +show_percentage = false + [node name="NextButton" type="Control" parent="."] visible = false anchor_left = 0.5 diff --git a/vessels/Projectile.gd b/vessels/Projectile.gd index 26281ae..eaae4ae 100644 --- a/vessels/Projectile.gd +++ b/vessels/Projectile.gd @@ -1,5 +1,7 @@ extends Area2D +const COMBAT_TARGET = preload("res://core/CombatTarget.gd") + # #86: faster. The bolt used to crawl far enough behind a moving target that # leading a shot was guesswork rather than skill. # #155: was 850, which took 1.1s to cross the gun's own range - long enough @@ -54,9 +56,9 @@ func _physics_process(delta: float) -> void: func _on_area_entered(area: Area2D) -> void: - # Asteroids answer take_damage() exactly as enemies do, so both are hit by - # the same branch rather than by a second special case. - if area.is_in_group("enemies") or area.is_in_group("asteroids"): + # Anything that opted into the weapon-target contract answers take_damage(), + # so adding a new target composition needs no projectile type check. + if area.is_in_group(COMBAT_TARGET.GROUP): area.take_damage(damage) queue_free() diff --git a/vessels/Ship.gd b/vessels/Ship.gd index b2fb789..d63bdfc 100644 --- a/vessels/Ship.gd +++ b/vessels/Ship.gd @@ -3,6 +3,7 @@ extends Area2D signal destroyed const VESSEL_DEFINITIONS = preload("res://vessels/VesselDefinitions.gd") +const COMBAT_TARGET = preload("res://core/CombatTarget.gd") @export var max_speed: float = 460.0 @export var acceleration: float = 450.0 @@ -200,17 +201,14 @@ func _breakaway_direction() -> Vector2: func _on_area_entered(area: Area2D) -> void: - if not (area.is_in_group("enemies") or area.is_in_group("asteroids")): + if not area.is_in_group(COMBAT_TARGET.GROUP): return - # The enemy is told about the impact BEFORE this ship resolves its own - # damage, because resolving it can free this node - and a ship that dies - # to a ramming chaser must still take the chaser with it. Whether the - # impact actually destroys the enemy is the enemy's decision (#82). - if area.is_in_group("enemies") and area.has_method("collide_with_player_vessel"): + # A target is told about the impact BEFORE this ship resolves its own + # damage, because resolving it can free this node. Whether the impact does + # anything to the target is part of that target's composition (#82). + if area.has_method("collide_with_player_vessel"): area.collide_with_player_vessel() - var absorbed: bool = take_damage(1) - if absorbed and area.is_in_group("asteroids") and is_instance_valid(area): - area.queue_free() + take_damage(1) func _draw() -> void: diff --git a/vessels/VesselGarrisonComponent.gd b/vessels/VesselGarrisonComponent.gd index c447b9f..e2846ca 100644 --- a/vessels/VesselGarrisonComponent.gd +++ b/vessels/VesselGarrisonComponent.gd @@ -1,6 +1,8 @@ class_name VesselGarrisonComponent extends Node +const COMBAT_TARGET = preload("res://core/CombatTarget.gd") + # #105 (#97g): "When a vessel is assigned to an outpost, it navigates around # the outpost by itself, attacking nearby ennemies if any are detected." # @@ -66,7 +68,7 @@ func is_engaging() -> bool: func tick(delta: float) -> void: var ship: Node2D = get_parent() - var target: Node2D = _nearest_enemy(ship) + var target: Node2D = _nearest_target(ship) _engaging = target != null if target == null: _patrol(ship, delta) @@ -176,20 +178,27 @@ func _hold_fire(ship: Node2D) -> void: weapon.set_fire_direction(Vector2.ZERO) -func _nearest_enemy(ship: Node2D) -> Node2D: +func _nearest_target(ship: Node2D) -> Node2D: var nearest: Node2D = null - var best: float = detection_range - for enemy in get_tree().get_nodes_in_group("enemies"): - if not is_instance_valid(enemy) or enemy.is_queued_for_deletion(): + var best_distance: float = INF + var best_priority: int = -1 + for target_node in get_tree().get_nodes_in_group(COMBAT_TARGET.GROUP): + if not is_instance_valid(target_node) or not (target_node is Node2D) \ + or target_node.is_queued_for_deletion(): continue - var at: Vector2 = (enemy as Node2D).global_position + var target: Node2D = target_node as Node2D + var at: Vector2 = target.global_position # Judged from the POST, not from the vessel: otherwise a garrison # that had drifted out to its leash would see further still, and # could be walked away from its outpost one enemy at a time. if station.distance_to(at) > leash_range + engage_distance: continue var d: float = ship.global_position.distance_to(at) - if d < best: - best = d - nearest = enemy + if d > detection_range: + continue + var priority: int = COMBAT_TARGET.priority(target) + if priority > best_priority or (priority == best_priority and d < best_distance): + best_priority = priority + best_distance = d + nearest = target return nearest diff --git a/vessels/VesselWeaponComponent.gd b/vessels/VesselWeaponComponent.gd index d54b4d1..3a05185 100644 --- a/vessels/VesselWeaponComponent.gd +++ b/vessels/VesselWeaponComponent.gd @@ -36,6 +36,7 @@ var fire_timer: float = 0.0 var fire_target: Vector2 = Vector2.ZERO var has_fire_target: bool = false var projectile_scene: PackedScene = preload("res://vessels/Projectile.tscn") +const COMBAT_TARGET = preload("res://core/CombatTarget.gd") # By path, not by class name - see the note at the top of MuzzleSparkVFX. const MUZZLE_SPARK = preload("res://vfx/MuzzleSparkVFX.gd") @@ -58,7 +59,6 @@ const ASSIST_CONE_DEGREES: float = 28.0 # smaller than the directional cone at ordinary weapon range: it catches a # pointer placed beside something without making the whole screen magnetic. const ASSIST_CURSOR_RADIUS: float = 180.0 -const ASSIST_TARGET_GROUP: StringName = &"player_aim_targets" # The concrete point an aim should stick to. A target can qualify in two ways: @@ -81,7 +81,7 @@ static func assisted_point(tree: SceneTree, origin: Vector2, point: Vector2, var cursor_distance: float = INF var direction_pick: Node2D = null var direction_distance: float = INF - for target_node in tree.get_nodes_in_group(ASSIST_TARGET_GROUP): + for target_node in tree.get_nodes_in_group(COMBAT_TARGET.GROUP): if not is_instance_valid(target_node) or not (target_node is Node2D) \ or target_node.is_queued_for_deletion(): continue diff --git a/world/Asteroid.gd b/world/Asteroid.gd index 0a85a57..a21a7e5 100644 --- a/world/Asteroid.gd +++ b/world/Asteroid.gd @@ -1,5 +1,7 @@ extends Area2D +const COMBAT_TARGET = preload("res://core/CombatTarget.gd") + # A drifting rock that can be shot apart for the ore inside it. # # Asteroids used to be pure hazard: they could only ever cost you a ship. Now @@ -32,7 +34,7 @@ var shape_points: PackedVector2Array = PackedVector2Array() func _ready() -> void: randomize() add_to_group("asteroids") - add_to_group("player_aim_targets") + COMBAT_TARGET.register(self, COMBAT_TARGET.RESOURCE_PRIORITY) area_entered.connect(_on_area_entered) var point_count: int = 7