spaceship/tests/test_building_separation.gd

135 lines
5.9 KiB
GDScript3
Raw Normal View History

#224: refuse to build a structure on top of another BuildingField.place() dropped a structure at the fleet centroid with no overlap check of any kind, so they could be stacked. It reads worse than it looks. BuildingField decides which structure the fleet is standing on by testing the centroid against interact_radius and returning the FIRST match, so two structures inside one another make that a question of list order rather than of where the player flew - you could fly to your workshop and open the crafter. The separation is interact_radius itself (150) rather than a new number, because that is exactly the circle the ambiguity comes from. Buildings draw to a radius of roughly 60, so this leaves a visible gap rather than letting them touch. Gated in three places: the info popup (the button greys out and says which structure is in the way), _confirm_build, and _try_place - the preview goes stale as the fleet keeps moving. Ordering matters in one case. A unique structure replaces itself by demolishing the old one BEFORE placing the new one, so a placement refused afterwards would leave the player with neither. The check runs before the demolition and excludes the structure being replaced. Case D pins it. Two existing tests encoded the old permissive behaviour and now fly clear first; neither is about placement: test_build_info_popup builds every menu option from one position, so each structure blocked the next test_payments_use_fleet_hold builds a mothership on the ship lab that an earlier case injects at the origin Also fixes a bug from 37e2e89: Sfx.warm() called add_child() from _ready(), which Godot refuses outright - it printed an error and never pre-built the pool, defeating the point of warming it. Deferred, like Music.ensure(). 173/173. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-09 12:57:28 +02:00
extends SceneTree
# Forgejo #224: a structure may not be built on top of one that is already
# there.
#
# It matters beyond the look of it. BuildingField decides which structure the
# fleet is standing on by testing the centroid against interact_radius and
# returning the FIRST match, so two structures inside one another make that a
# question of list order rather than of where the player flew - you would fly
# to your workshop and open the crafter.
#
# Covers:
# A. the spot a structure already occupies is refused
# B. clear of it, the same build goes through
# C. a unique structure can still be rebuilt where the old one stands,
# because the old one is what is being replaced
# D. and a REFUSED rebuild does not demolish the existing one first
var game_state: Node
var scene: Node
var controller: Node
var fleet: Node2D
var elapsed: float = 0.0
var stage: int = 0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.reset_progression()
game_state.reset_new_game()
game_state.intro_active = false
game_state.reset_quests()
game_state.rocks = 100000
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 100000)
game_state.bank_inventory.set_amount(GameState.RESOURCE_ENERGY, 100000)
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
controller = scene.get_node("BuildController")
fleet = scene.get_node("Fleet")
func _process(delta: float) -> bool:
elapsed += delta
if elapsed < 0.4:
return false
var sep: float = GameState.MIN_BUILDING_SEPARATION
# _confirm_build() returns SILENTLY on a locked structure, which would
# make every refusal below look like the separation rule working when it
# was only the unlock gate. Unlock what this test builds, and assert it.
for type in [GameState.BUILDING_COMMAND_CENTER, GameState.BUILDING_MOTHERSHIP,
GameState.BUILDING_SHIP_CRAFTER]:
var node_id: String = game_state.research_node_for_building(type)
if node_id != "" and not game_state.is_unlocked(node_id):
game_state.unlocked.append(node_id)
if not game_state.is_building_unlocked(type):
return _fail("'%s' is still locked, so a refusal would prove nothing" % type)
# --- A. the occupied spot is refused --------------------------------
game_state.buildings.clear()
game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2.ZERO)
if game_state.building_blocking(Vector2.ZERO).is_empty():
return _fail("a structure's own position reads as a clear spot")
# Just inside the separation, and just outside it.
if game_state.building_blocking(Vector2(sep - 1.0, 0.0)).is_empty():
return _fail("a spot %.0f units away is not blocked, but the rule is %.0f" % [sep - 1.0, sep])
if not game_state.building_blocking(Vector2(sep + 1.0, 0.0)).is_empty():
return _fail("a spot %.0f units away is blocked, but the rule is %.0f" % [sep + 1.0, sep])
var blocker: Dictionary = game_state.building_blocking(Vector2.ZERO)
if String(blocker.get("type", "")) != GameState.BUILDING_SHIP_CRAFTER:
return _fail("the blocking structure is reported as '%s'" % blocker.get("type", ""))
print("a spot inside %.0f units of a structure is refused, outside it is clear" % sep)
# --- B. and the whole build path agrees -----------------------------
# On top of the crafter: refused, and nothing is built or spent.
fleet.teleport_to(Vector2.ZERO)
var before: int = game_state.buildings.size()
controller.pending_build_type = GameState.BUILDING_COMMAND_CENTER
controller._confirm_build()
if game_state.buildings.size() != before:
return _fail("a structure was built on top of the crafter")
print("the build path refuses it too: %s" % controller.build_menu.status.text)
# Clear of it: the same build goes through.
fleet.teleport_to(Vector2(sep * 3.0, 0.0))
controller.pending_build_type = GameState.BUILDING_COMMAND_CENTER
controller._confirm_build()
if game_state.buildings.size() != before + 1:
return _fail("the same build was refused on a clear spot")
print("clear of it, the same build goes through")
# --- C. a unique structure replaces itself in place -----------------
game_state.buildings.clear()
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
fleet.teleport_to(Vector2.ZERO)
if not game_state.building_blocking(Vector2.ZERO, GameState.BUILDING_MOTHERSHIP).is_empty():
return _fail("a unique structure blocks its own replacement")
controller.pending_build_type = GameState.BUILDING_MOTHERSHIP
controller._confirm_build()
if game_state.buildings.size() != 1:
return _fail("rebuilding the mothership in place left %d structures" % game_state.buildings.size())
if String(game_state.buildings[0].type) != GameState.BUILDING_MOTHERSHIP:
return _fail("the rebuilt structure is not a mothership")
print("a unique structure can still be rebuilt where the old one stood")
# --- D. a refused rebuild demolishes nothing ------------------------
# The mothership is unique and is removed before the new one is placed.
# Something ELSE in the way has to be caught before that demolition, or
# the player is left with neither.
game_state.buildings.clear()
var keep: int = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(sep * 4.0, 0.0))
fleet.teleport_to(Vector2(sep * 4.0, 0.0))
controller.pending_build_type = GameState.BUILDING_MOTHERSHIP
controller._confirm_build()
if game_state.find_building(keep).is_empty():
return _fail("a refused rebuild demolished the existing mothership - the player is left with neither")
if game_state.buildings.size() != 2:
return _fail("a refused rebuild changed the structure count to %d" % game_state.buildings.size())
print("a refused rebuild leaves the existing structure standing")
print("TEST_BUILDING_SEPARATION: PASS")
quit(0)
return true
func _fail(message: String) -> bool:
print("TEST_BUILDING_SEPARATION: FAIL (%s)" % message)
quit(1)
return true