From 798deb48a15a58dde958ee8d39d4cbd57235d28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Thu, 13 Aug 2026 21:39:05 +0200 Subject: [PATCH] Animate ship crafter deliveries --- GameState.gd | 33 ++++++++++++++++++++-- Main.tscn | 6 +++- VesselDelivery.gd | 16 +++++++++++ VesselDeliveryField.gd | 37 +++++++++++++++++++++++++ tests/test_vessel_delivery.gd | 52 +++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 VesselDelivery.gd create mode 100644 VesselDeliveryField.gd create mode 100644 tests/test_vessel_delivery.gd diff --git a/GameState.gd b/GameState.gd index 1383eb3..b95049b 100644 --- a/GameState.gd +++ b/GameState.gd @@ -308,6 +308,11 @@ var next_building_id: int = 1 # Production is simulated here, in the autoload, the same way extractor # haulers are, so a build finishes whether or not the player is in space. var stored_vessels: Array = [] +# Finished crafter orders travel to the mothership before appearing in its +# hangar. State is authoritative here so delivery continues off-screen. +# {"type", "from", "to", "progress"} +var vessel_deliveries: Array = [] +const VESSEL_DELIVERY_SPEED: float = 950.0 signal order_completed(building_id: int, type: String) @@ -695,9 +700,15 @@ func _advance_production(delta: float) -> void: # hub and wait for work (#19). _station_transporter(b.position) else: - # Everything else is delivered to the mothership and waits there - # until the player crews it (#23). - stored_vessels.append(product) + # A finished hull launches from the crafter and physically flies to + # the mothership hangar. With no mothership there is nowhere to fly, + # so preserve the old safe fallback of holding it immediately. + var mothership: Variant = get_mothership_position() + if mothership == null: + stored_vessels.append(product) + else: + vessel_deliveries.append({"type": product, "from": b.position, + "to": mothership, "progress": 0.0}) order_completed.emit(int(b.id), product) @@ -818,6 +829,7 @@ func _process(delta: float) -> void: # Runs in the autoload, so mining and transport continue while the player # is on a planet's surface or nowhere near either. _advance_production(delta) + _advance_vessel_deliveries(delta) for e in extractors: e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE var whole: int = int(floor(float(e.mining_progress))) @@ -827,6 +839,21 @@ func _process(delta: float) -> void: _advance_transporters(delta) +func vessel_delivery_position(delivery: Dictionary) -> Vector2: + return (delivery.from as Vector2).lerp(delivery.to as Vector2, + clampf(float(delivery.progress), 0.0, 1.0)) + + +func _advance_vessel_deliveries(delta: float) -> void: + for i in range(vessel_deliveries.size() - 1, -1, -1): + var delivery: Dictionary = vessel_deliveries[i] + var distance: float = (delivery.from as Vector2).distance_to(delivery.to as Vector2) + delivery.progress = float(delivery.progress) + delta / maxf(distance / VESSEL_DELIVERY_SPEED, 0.001) + if float(delivery.progress) >= 1.0: + stored_vessels.append(String(delivery.type)) + vessel_deliveries.remove_at(i) + + # --- transporters (#19) --------------------------------------------------- # Vessels stationed at a transport hub that fetch ore from the mothership and diff --git a/Main.tscn b/Main.tscn index 4f65ca6..9493281 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=22 format=3] +[gd_scene load_steps=23 format=3] [ext_resource type="Script" path="res://Starfield.gd" id="1"] [ext_resource type="Script" path="res://Fleet.gd" id="2"] @@ -24,6 +24,7 @@ [ext_resource type="Script" path="res://TransporterField.gd" id="23"] [ext_resource type="Script" path="res://ResourceBar.gd" id="24"] [ext_resource type="Script" path="res://GasCounter.gd" id="25"] +[ext_resource type="Script" path="res://VesselDeliveryField.gd" id="26"] [sub_resource type="ShaderMaterial" id="1"] shader = ExtResource("5") @@ -74,6 +75,9 @@ script = ExtResource("14") [node name="TransporterField" type="Node2D" parent="."] script = ExtResource("23") +[node name="VesselDeliveryField" type="Node2D" parent="."] +script = ExtResource("26") + [node name="BuildController" type="Node2D" parent="."] script = ExtResource("15") diff --git a/VesselDelivery.gd b/VesselDelivery.gd new file mode 100644 index 0000000..4c07a6d --- /dev/null +++ b/VesselDelivery.gd @@ -0,0 +1,16 @@ +extends Node2D + +# Visual-only craft-to-mothership flight. GameState owns position/progress; +# this node just renders a finished hull headed toward the hangar. +var vessel_type: String = GameState.VESSEL_INTERCEPTOR + + +func _ready() -> void: + z_index = -1 + queue_redraw() + + +func _draw() -> void: + var color: Color = Color(0.45, 0.8, 1.0) if vessel_type == GameState.VESSEL_SHIELD else Color(0.95, 0.55, 0.85) if vessel_type == GameState.VESSEL_TRANSPORTER else Color(0.25, 0.85, 1.0) + var hull := PackedVector2Array([Vector2(0, -20), Vector2(13, 15), Vector2(0, 9), Vector2(-13, 15)]) + NeonDraw.draw_glowing_polygon(self, hull, color, 1.0) diff --git a/VesselDeliveryField.gd b/VesselDeliveryField.gd new file mode 100644 index 0000000..606b1cf --- /dev/null +++ b/VesselDeliveryField.gd @@ -0,0 +1,37 @@ +extends Node2D + +@export var visible_radius: float = 2600.0 +@onready var fleet: Node2D = get_node("../Fleet") +var delivery_script: Script = preload("res://VesselDelivery.gd") +var active: Dictionary = {} + + +func _process(_delta: float) -> void: + var centre: Vector2 = fleet.get_centroid() + for i in range(GameState.vessel_deliveries.size()): + var delivery: Dictionary = GameState.vessel_deliveries[i] + var pos := GameState.vessel_delivery_position(delivery) + if pos.distance_to(centre) > visible_radius: + _despawn(i) + continue + var node: Node2D = active.get(i) + if node == null or not is_instance_valid(node): + node = delivery_script.new() + get_parent().add_child(node) + active[i] = node + node.vessel_type = String(delivery.type) + node.position = pos + var direction: Vector2 = (delivery.to as Vector2) - (delivery.from as Vector2) + if direction.length() > 0.1: + node.rotation = direction.angle() + PI / 2.0 + node.queue_redraw() + for key in active.keys(): + if key >= GameState.vessel_deliveries.size(): + _despawn(key) + + +func _despawn(index: int) -> void: + var node: Node2D = active.get(index) + if node != null and is_instance_valid(node): + node.queue_free() + active.erase(index) diff --git a/tests/test_vessel_delivery.gd b/tests/test_vessel_delivery.gd new file mode 100644 index 0000000..cf518cb --- /dev/null +++ b/tests/test_vessel_delivery.gd @@ -0,0 +1,52 @@ +extends SceneTree + +# Forgejo #28: a completed crafter order visibly travels to the mothership, +# rather than appearing in the hangar at the instant the build timer ends. +var game_state: Node +var scene: Node +var stage := 0 +var elapsed := 0.0 + + +func _initialize() -> void: + game_state = root.get_node("GameState") + game_state.buildings.clear() + game_state.extractors.clear() + game_state.transporters.clear() + game_state.vessel_deliveries.clear() + game_state.stored_vessels.clear() + game_state.build_time_scale = 0.02 + game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) + game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(900, 0)) + game_state.deposit_to_building(2, GameState.SHIP_COST) + game_state.queue_order(2, GameState.PRODUCT_SHIP) + scene = load("res://Main.tscn").instantiate() + root.add_child(scene) + + +func _process(delta: float) -> bool: + elapsed += delta + if stage == 0 and not game_state.vessel_deliveries.is_empty(): + if not game_state.stored_vessels.is_empty(): + return _fail("ship reached hangar before its delivery flight") + var field: Node = scene.get_node("VesselDeliveryField") + if field.active.is_empty(): + return _fail("delivery flight was not rendered") + print("finished ship launched from crafter toward mothership") + stage = 1 + if stage == 1 and game_state.vessel_deliveries.is_empty(): + if game_state.stored_vessels.size() != 1: + return _fail("delivered ship did not arrive in hangar") + print("delivery arrived in mothership hangar") + print("TEST_VESSEL_DELIVERY: PASS") + quit(0) + return true + if elapsed > 8.0: + return _fail("delivery timed out") + return false + + +func _fail(message: String) -> bool: + print("TEST_VESSEL_DELIVERY: FAIL (%s)" % message) + quit(1) + return true