Repair a broken working tree, and make every crafter press report

The crafter buttons doing nothing was not the crafter: Ship.gd was left
mid-edit and failed to parse (undeclared SHIELD_COLOR/SHIELD_RADIUS, and
a spawn() that only exists on the concrete VFX subclass). A script that
does not compile takes Main.tscn's whole node tree with it, so every
button in the game was dead. The project directory is the one being
played from, so an unfinished edit there is a broken build - finish or
revert before stopping.

With that fixed, the real gap underneath: an order the crafter cannot
pay for is ACCEPTED and queued (#16), which is indistinguishable from a
dead button. Ordering now reports either "building now" or "queued - N
more ore must be delivered here", and the transient message is rendered
LAST so it is the first line read rather than buried under the standing
fleet-capacity note.

test_crafter_feedback covers all three presses and asserts the message
survives the per-frame refresh, which is the specific way this looked
broken before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-11 22:50:09 +02:00
commit 004808f067
6 changed files with 413 additions and 14 deletions

94
Ship.gd
View file

@ -17,6 +17,21 @@ signal destroyed
@export var idle_rescan_interval: float = 0.25
@export var projectile_damage: int = 1
# Which kind of vessel this is (GameState.VESSEL_*). Drives both its
# researched statistics and whether it carries a shield (#5).
@export var vessel_type: String = "interceptor"
# --- shield (#5) ----------------------------------------------------------
const SHIELD_COLOR: Color = Color(0.45, 0.8, 1.0)
const SHIELD_RADIUS: float = 30.0
# A shield vessel survives a hit that would destroy any other ship, and is
# then defenceless until it cycles back up. Personal, not a bubble: it
# protects the vessel carrying it and nothing else.
var shield_charges: int = 0
var shield_recharge_left: float = 0.0
var _shield_ring: Node2D = null
# Neon colours, assigned by Fleet on spawn so a scene can tint its fleet.
# The lunar surface is a bright grey, where the default near-white core all
# but vanishes - PlanetSurface.tscn passes a dark core there. See NeonDraw.
@ -46,6 +61,10 @@ func _ready() -> void:
move_target = position
add_to_group("player_ships")
area_entered.connect(_on_area_entered)
if vessel_type == GameState.VESSEL_SHIELD:
_build_shield_ring()
shield_charges = GameState.shield_max_charges()
apply_vessel_stats()
queue_redraw()
@ -53,7 +72,49 @@ func set_move_target(world_pos: Vector2) -> void:
move_target = world_pos
func apply_vessel_stats() -> void:
# Read from GameState rather than baked in at spawn, so researching an
# upgrade improves the ships already flying and not just the next one
# built. Fleet calls this on every vessel when something is bought.
max_speed = GameState.ship_max_speed(vessel_type)
acceleration = GameState.ship_acceleration(vessel_type)
fire_range = GameState.ship_fire_range(vessel_type)
fire_cooldown = GameState.ship_fire_cooldown(vessel_type)
projectile_damage = GameState.ship_damage(vessel_type)
if vessel_type == GameState.VESSEL_SHIELD:
# Topping up a live shield with researched capacity, without
# refilling one that is currently down and cycling.
var was_up: bool = shield_charges > 0
shield_charges = GameState.shield_max_charges() if was_up else shield_charges
_update_shield_visual()
func has_shield() -> bool:
return vessel_type == GameState.VESSEL_SHIELD and shield_charges > 0
func _update_shield_visual() -> void:
if _shield_ring == null:
return
# visible only - per #10 the ring is rasterised once and never redrawn to
# turn it on and off.
_shield_ring.visible = has_shield()
func _tick_shield(delta: float) -> void:
if vessel_type != GameState.VESSEL_SHIELD:
return
if shield_charges >= GameState.shield_max_charges():
return
shield_recharge_left -= delta
if shield_recharge_left <= 0.0:
shield_charges += 1
shield_recharge_left = GameState.shield_recharge_time()
_update_shield_visual()
func _physics_process(delta: float) -> void:
_tick_shield(delta)
var to_target: Vector2 = move_target - position
var distance: float = to_target.length()
@ -122,10 +183,35 @@ func _find_nearest_enemy() -> Node2D:
func _on_area_entered(area: Area2D) -> void:
if area.is_in_group("enemies") or area.is_in_group("asteroids"):
ShipExplosionVFX.spawn(get_tree().root, position)
destroyed.emit()
queue_free()
if not (area.is_in_group("enemies") or area.is_in_group("asteroids")):
return
if has_shield():
# Absorbed. The thing that hit it is still destroyed - a shield stops
# the vessel dying, it does not make the rock harmless - but the
# vessel survives and is defenceless until the shield cycles back.
shield_charges -= 1
shield_recharge_left = GameState.shield_recharge_time()
_update_shield_visual()
if area.is_in_group("asteroids") and is_instance_valid(area):
area.queue_free()
return
ShipExplosionVFX.spawn(get_tree().root, position)
destroyed.emit()
queue_free()
func _build_shield_ring() -> void:
# Its own child, drawn once, toggled with `visible` (#10). A shield that
# came and went by redrawing the ship would re-tessellate the hull every
# time it cycled.
_shield_ring = NeonPiece.create(self, Vector2.ZERO, SHIELD_COLOR)
var points: PackedVector2Array = PackedVector2Array()
for i in range(14):
var a: float = (float(i) / 14.0) * TAU
points.append(Vector2(cos(a), sin(a)) * SHIELD_RADIUS)
_shield_ring.add_polygon(points, 0.7)
_shield_ring.queue_redraw()
_shield_ring.visible = false
func _draw() -> void: