diff --git a/GameState.gd b/GameState.gd index 5366a49..3854f56 100644 --- a/GameState.gd +++ b/GameState.gd @@ -1155,6 +1155,40 @@ func _dispatch_transporters() -> void: var mothership: Variant = get_mothership_position() if mothership == null: return # nowhere to load from + # Supply already-queued production and research before collecting passive + # extractor output. Extractors can refill forever, so servicing them first + # can otherwise consume every transporter each time one becomes idle and + # permanently starve a partly supplied order. + for b in buildings: + var needed_resource := "" + for resource_type in resource_types(): + if outstanding_resource_cost(int(b.id), resource_type) > 0 \ + and bank_inventory.amount(resource_type) > 0: + needed_resource = resource_type + break + if needed_resource == "": + continue + # One at a time per building: without this every idle transporter + # would be sent after the same shortfall and they would all arrive + # carrying resources nobody needs any more. + if _transporter_inbound_to(int(b.id)): + continue + var chosen: Dictionary = {} + var best_d: float = INF + for t in transporters: + if t.state != TRANSPORTER_IDLE: + continue + var here: Vector2 = transporter_position(t) + var d: float = here.distance_squared_to(b.position) + if d < best_d: + best_d = d + chosen = t + if chosen.is_empty(): + return # every transporter is busy + chosen.target_id = int(b.id) + chosen.resource_type = needed_resource + _begin_leg(chosen, transporter_position(chosen), mothership, TRANSPORTER_TO_MOTHERSHIP) + # A distant extractor is worth visiting only once it has a full hold ready. # This keeps the transporter system meaningful: mining is local until a # vessel physically carries a researched-sized load home. @@ -1180,35 +1214,6 @@ func _dispatch_transporters() -> void: chosen_extractor.target_id = 0 _begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position, TRANSPORTER_TO_EXTRACTOR) - for b in buildings: - var needed_resource := "" - for resource_type in resource_types(): - if outstanding_resource_cost(int(b.id), resource_type) > 0 \ - and bank_inventory.amount(resource_type) > 0: - needed_resource = resource_type - break - if needed_resource == "": - continue - # One at a time per building: without this every idle transporter - # would be sent after the same shortfall and they would all arrive - # carrying ore nobody needs any more. - if _transporter_inbound_to(int(b.id)): - continue - var chosen: Dictionary = {} - var best_d: float = INF - for t in transporters: - if t.state != TRANSPORTER_IDLE: - continue - var here: Vector2 = transporter_position(t) - var d: float = here.distance_squared_to(b.position) - if d < best_d: - best_d = d - chosen = t - if chosen.is_empty(): - return # every transporter is busy - chosen.target_id = int(b.id) - chosen.resource_type = needed_resource - _begin_leg(chosen, transporter_position(chosen), mothership, TRANSPORTER_TO_MOTHERSHIP) func _advance_transporters(delta: float) -> void: diff --git a/VesselGasBomberComponent.gd b/VesselGasBomberComponent.gd index bdff8a5..dc44815 100644 --- a/VesselGasBomberComponent.gd +++ b/VesselGasBomberComponent.gd @@ -10,6 +10,8 @@ const COOLING_COLOR := Color(0.38, 0.16, 0.32) func setup() -> void: + # Keep the ship's normal neon outline above the charge fill. + show_behind_parent = true queue_redraw() @@ -37,18 +39,46 @@ func cooldown_progress() -> float: return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001) +func _filled_hull(points: PackedVector2Array, progress: float) -> PackedVector2Array: + if points.size() < 3 or progress <= 0.001: + return PackedVector2Array() + if progress >= 0.999: + return points.duplicate() + var top := INF + var bottom := -INF + for point in points: + top = minf(top, point.y) + bottom = maxf(bottom, point.y) + var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0)) + var clipped := PackedVector2Array() + var previous: Vector2 = points[-1] + var previous_inside := previous.y >= fill_y + for current in points: + var current_inside := current.y >= fill_y + if current_inside != previous_inside: + var amount: float = (fill_y - previous.y) / (current.y - previous.y) + clipped.append(previous.lerp(current, amount)) + if current_inside: + clipped.append(current) + previous = current + previous_inside = current_inside + return clipped + + func _draw() -> void: var parent_ship: Node = get_parent() - var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE - if is_ready(): - # Bright, complete halo: this bomber owns one available shot. - NeonDraw.draw_glowing_circle(self, Vector2.ZERO, 31.0, READY_COLOR, 0.9, core) - draw_circle(Vector2.ZERO, 4.0, Color(READY_COLOR, 0.5)) + if parent_ship == null or parent_ship.definition == null: return - # A muted full ring says "not ready"; the bright arc fills clockwise so - # the individual bomber also communicates how close it is to rearming. - draw_arc(Vector2.ZERO, 31.0, 0.0, TAU, 48, Color(COOLING_COLOR, 0.65), 4.0, true) - var progress := cooldown_progress() - if progress > 0.001: - draw_arc(Vector2.ZERO, 31.0, -PI / 2.0, - -PI / 2.0 + TAU * progress, 48, Color(READY_COLOR, 0.92), 4.0, true) + var core: Color = parent_ship.core_color if parent_ship != null else Color.WHITE + var hull: PackedVector2Array = parent_ship.definition.hull_points + if is_ready(): + # A fully lit hull means this individual bomber owns an available shot. + draw_colored_polygon(hull, Color(READY_COLOR, 0.42)) + NeonDraw.draw_glowing_polygon(self, hull, READY_COLOR, 0.75, core, 0.75) + return + # Rearming is shown inside the bomber instead of as a shield-like ring: + # the muted silhouette fills from its engines toward its nose. + draw_colored_polygon(hull, Color(COOLING_COLOR, 0.58)) + var filled := _filled_hull(hull, cooldown_progress()) + if filled.size() >= 3: + draw_colored_polygon(filled, Color(READY_COLOR, 0.68)) diff --git a/tests/_gas_bomb_aim_preview.gd b/tests/_gas_bomb_aim_preview.gd index 18bb8dc..8a38362 100644 --- a/tests/_gas_bomb_aim_preview.gd +++ b/tests/_gas_bomb_aim_preview.gd @@ -21,8 +21,10 @@ func _ready() -> void: var skill: GasBombController = main.get_node("GasBombController") skill._process(0.0) # Show both per-ship states in one frame: the first bomber is rearming, - # while the second has the bright complete ready halo and supplies the aim. - fleet.ships[2].get_component("gas_bomber").consume_shot(GasBombController.COOLDOWN) + # while the second has a fully lit hull and supplies the aim. + var rearming: Node = fleet.ships[2].get_component("gas_bomber") + rearming.consume_shot(GasBombController.COOLDOWN) + rearming.tick(GasBombController.COOLDOWN * 0.5) main.get_node("Camera2D").zoom = Vector2(1.8, 1.8) var button: Control = main.get_node("UI/GasBombButton") var press := InputEventScreenTouch.new() diff --git a/tests/test_gas_bomber_cooldowns.gd b/tests/test_gas_bomber_cooldowns.gd index 47562ee..2a25403 100644 --- a/tests/test_gas_bomber_cooldowns.gd +++ b/tests/test_gas_bomber_cooldowns.gd @@ -37,6 +37,9 @@ func _process(delta: float) -> bool: skill._process(0.0) if first.is_ready() or not second.is_ready() or not skill.button.enabled: return _fail("the first shot disabled more than its firing bomber") + if not first._filled_hull(fleet.ships[0].definition.hull_points, 0.0).is_empty() \ + or first._filled_hull(fleet.ships[0].definition.hull_points, 0.5).size() < 3: + return _fail("bomber cooldown did not produce an empty-to-filling hull") if skill.projectiles.size() != 1: return _fail("the first bomber did not launch exactly one projectile") diff --git a/tests/test_multi_resource_supply.gd b/tests/test_multi_resource_supply.gd index 45a016a..d29b574 100644 --- a/tests/test_multi_resource_supply.gd +++ b/tests/test_multi_resource_supply.gd @@ -9,7 +9,7 @@ func _initialize() -> void: state.stored_vessels.clear() state.reset_progression() state.rocks = 0 - state.gas = 30 + state.gas = 10 state.build_time_scale = 0.01 state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) @@ -18,8 +18,20 @@ func _initialize() -> void: if not state.queue_lab_research(lab_id, "unlock_gas_bomber"): _fail("could not queue gas research") return - if state.outstanding_resource_cost(lab_id, GameState.RESOURCE_GAS) != 30: - _fail("lab did not request 30 gas") + # Reproduce a first 20-unit delivery followed by the remaining 10 being + # available at the mothership. Full extractors must not take priority over + # completing this already-partly-supplied research order. + state.deposit_resource_to_building(lab_id, GameState.RESOURCE_GAS, 20) + state.add_extractor("Ore A", Vector2(240, 0), GameState.RESOURCE_ORE) + state.add_extractor("Ore B", Vector2(360, 0), GameState.RESOURCE_ORE) + for extractor in state.extractors: + extractor.inventory.deposit(GameState.RESOURCE_ORE, state.transporter_load_limit()) + if state.outstanding_resource_cost(lab_id, GameState.RESOURCE_GAS) != 10: + _fail("partly supplied lab did not request its remaining 10 gas") + return + state._dispatch_transporters() + if not state._transporter_inbound_to(lab_id): + _fail("extractor collection took priority over waiting research") return # Large steps make each physical leg complete in one call; multiple calls