The previous ten tickets shipped with almost no coverage - only #109, #113, #115 and #117 touched a test. That was a gap, not a decision: everything before it had its behaviour pinned, and #116 in particular added a whole subsystem nothing was watching. Four new files and one extended case: test_spacial_workshop #116 - the repair economy end to end: the cost of a repair scales with the damage, the workshop asks logistics for that ore and stops at its buffer, a drone spends when it LEAVES so two cannot double-charge one wall, and a target levelled in flight sends its load home rather than swallowing it test_desktop_controls #118/#121 - both driven through the real routing, because the routing is what was missing; a test calling handle_mouse_button() itself would have passed against both reports test_enemy_bomber #114 - it flies, it faces where it flies, its bombs arrive, and it bombs structures with no fleet in sight test_menu_polish #111/#112/#119/#120 - what each panel shows test_wave_system (case G) #110 - the dev button, and that it does not exist outside dev mode One assertion worth recording: the craft caption is checked as an EXACT match against the vessel's display name rather than by hunting for resource words in it. The first version failed on "Gas Bomber" - a vessel named after a resource, and always will be. What #111 removed was the price spelled out underneath, not the name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
202 lines
9.2 KiB
GDScript
202 lines
9.2 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless test: run with
|
|
# godot --headless -s res://tests/test_spacial_workshop.gd
|
|
# Forgejo #116: the Spacial Workshop and its repair drones.
|
|
#
|
|
# Modelled on the transport hub, so most of what it does is already
|
|
# understood: drones belong to a workshop, are provisioned when it is built,
|
|
# are never a production order, and are upgraded at a ship lab. What is new
|
|
# is what a repair COSTS - a share of the building's own price, in
|
|
# proportion to the health it is missing.
|
|
#
|
|
# Covers:
|
|
# A. the building is in the catalog behind its own research, with its own
|
|
# look, and it provisions drones the moment it is placed
|
|
# B. the drone is a catalogued vessel that CANNOT be crafted, and carries
|
|
# its own upgrades
|
|
# C. a full repair costs a share of the building's price scaled by the
|
|
# damage: undamaged is free, half-wrecked is about half
|
|
# D. the workshop asks hub logistics for the ore its repairs will need,
|
|
# and stops asking past its buffer
|
|
# E. a drone flies out, spends ore, and puts structure back
|
|
# F. it will not go with an empty till, and two drones never chase the
|
|
# same wall
|
|
# G. a building demolished mid-flight gets its ore back rather than
|
|
# swallowing it
|
|
# H. the berth upgrade reaches workshops already standing
|
|
|
|
var game_state: Node
|
|
var workshop_id: int = 0
|
|
var crafter_id: int = 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
# GameState is an autoload: a bare GameState.<instance member> does not
|
|
# compile in a SceneTree script, so the node is fetched by name instead.
|
|
game_state = root.get_node("GameState")
|
|
game_state.buildings.clear()
|
|
game_state.extractors.clear()
|
|
game_state.transporters.clear()
|
|
game_state.repair_drones.clear()
|
|
game_state.reset_progression()
|
|
game_state.reset_waves()
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
print("TEST_SPACIAL_WORKSHOP: FAIL (%s)" % message)
|
|
quit(1)
|
|
return true
|
|
|
|
|
|
func _drones_of(id: int) -> Array:
|
|
var out: Array = []
|
|
for drone in game_state.repair_drones:
|
|
if int(drone.get("workshop_id", 0)) == id:
|
|
out.append(drone)
|
|
return out
|
|
|
|
|
|
# Runs the autoload's own repair clock without waiting out the real seconds.
|
|
func _simulate(seconds: float) -> void:
|
|
var step: float = 1.0 / 60.0
|
|
for i in range(int(seconds / step)):
|
|
game_state._advance_repairs(step)
|
|
|
|
|
|
func _process(_delta: float) -> bool:
|
|
var definitions: Script = load("res://buildings/BuildingDefinitions.gd")
|
|
|
|
# --- A. catalogued, researched, and it brings its own drones ---------
|
|
var workshop_def: Resource = definitions.get_definition(GameState.BUILDING_WORKSHOP)
|
|
if workshop_def == null:
|
|
return _fail("no \"spacial_workshop\" definition in the catalog")
|
|
if workshop_def.visual_scene == null or workshop_def.menu_button_name == "":
|
|
return _fail("the workshop has no visual or no build-menu entry")
|
|
for other in definitions.all():
|
|
if other.definition_id != GameState.BUILDING_WORKSHOP \
|
|
and other.menu_icon == workshop_def.menu_icon:
|
|
return _fail("the workshop shares its icon with the %s" % other.definition_id)
|
|
if game_state.is_building_unlocked(GameState.BUILDING_WORKSHOP):
|
|
return _fail("the workshop is buildable before its node is researched")
|
|
var research: String = ""
|
|
for node in GameState.SKILL_TREE:
|
|
if node.get("unlocks", "") == GameState.BUILDING_WORKSHOP:
|
|
research = String(node.id)
|
|
if research == "":
|
|
return _fail("nothing in the skill tree unlocks the workshop")
|
|
game_state.unlocked.append(research)
|
|
|
|
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
|
workshop_id = game_state.add_building(GameState.BUILDING_WORKSHOP, Vector2(600.0, 0.0))
|
|
if _drones_of(workshop_id).size() != GameState.BASE_WORKSHOP_DRONES:
|
|
return _fail("a new workshop stationed %d drones, expected %d" \
|
|
% [_drones_of(workshop_id).size(), GameState.BASE_WORKSHOP_DRONES])
|
|
print("workshop researched at \"%s\", arrives with %d drones" \
|
|
% [research, GameState.BASE_WORKSHOP_DRONES])
|
|
|
|
# --- B. the drone is a vessel, but not a product ---------------------
|
|
var drone_def: Resource = game_state.vessel_definition(GameState.REPAIR_DRONE)
|
|
if drone_def == null:
|
|
return _fail("no repair_drone in the vessel catalog")
|
|
if bool(drone_def.fleet_eligible):
|
|
return _fail("the repair drone is fleet-eligible - it would be craftable")
|
|
if drone_def.upgrades.is_empty():
|
|
return _fail("the repair drone carries no ship-lab upgrades")
|
|
if game_state.queue_order(workshop_id, GameState.REPAIR_DRONE):
|
|
return _fail("a repair drone was accepted as a production order")
|
|
print("repair drone: %d lab upgrades, not craftable" % drone_def.upgrades.size())
|
|
|
|
# --- C. what a repair costs -----------------------------------------
|
|
crafter_id = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(900.0, 0.0))
|
|
var price: int = int(definitions.get_definition(GameState.BUILDING_SHIP_CRAFTER).cost)
|
|
if game_state.repair_cost(crafter_id) != 0:
|
|
return _fail("an undamaged crafter costs %d to repair" % game_state.repair_cost(crafter_id))
|
|
var full: int = game_state.building_max_hp(GameState.BUILDING_SHIP_CRAFTER)
|
|
game_state.damage_building(crafter_id, full / 2)
|
|
var half_cost: int = game_state.repair_cost(crafter_id)
|
|
if absf(float(half_cost) - float(price) * 0.5) > float(price) * 0.12:
|
|
return _fail("a half-wrecked crafter costs %d of its %d price, expected about half" \
|
|
% [half_cost, price])
|
|
print("crafter at %d%% health: %d ore of its %d price" \
|
|
% [int(game_state.building_health_fraction(crafter_id) * 100.0), half_cost, price])
|
|
|
|
# --- D. and it asks for that ore ------------------------------------
|
|
var asked: int = game_state.building_requested_resource(workshop_id, GameState.RESOURCE_ORE)
|
|
if asked <= 0:
|
|
return _fail("a workshop with a wrecked crafter beside it asks for no ore")
|
|
if asked > GameState.WORKSHOP_ORE_BUFFER:
|
|
return _fail("it asked for %d ore, past its %d buffer" \
|
|
% [asked, GameState.WORKSHOP_ORE_BUFFER])
|
|
print("workshop asks hub logistics for %d ore" % asked)
|
|
|
|
# --- F (first half). an empty till sends nobody ----------------------
|
|
_simulate(4.0)
|
|
for drone in _drones_of(workshop_id):
|
|
if String(drone.state) != GameState.DRONE_IDLE:
|
|
return _fail("a drone left with no ore in the workshop")
|
|
if game_state.building_health_fraction(crafter_id) > 0.51:
|
|
return _fail("the crafter was repaired out of an empty workshop")
|
|
print("empty till: every drone stayed home")
|
|
|
|
# --- E. paid for, it flies and mends --------------------------------
|
|
game_state.deposit_resource_to_building(workshop_id, GameState.RESOURCE_ORE, 60)
|
|
var ore_before: int = game_state.building_resource(workshop_id, GameState.RESOURCE_ORE)
|
|
var hp_before: int = game_state.building_hp(crafter_id)
|
|
game_state._advance_repairs(0.016)
|
|
var flying: int = 0
|
|
for drone in _drones_of(workshop_id):
|
|
if String(drone.state) == GameState.DRONE_OUTBOUND:
|
|
flying += 1
|
|
if flying != 1:
|
|
return _fail("%d drones set off for one damaged crafter" % flying)
|
|
# --- F (second half). the ore leaves with the drone ------------------
|
|
if game_state.building_resource(workshop_id, GameState.RESOURCE_ORE) >= ore_before:
|
|
return _fail("a drone left without the ore being taken")
|
|
_simulate(30.0)
|
|
var hp_after: int = game_state.building_hp(crafter_id)
|
|
if hp_after <= hp_before:
|
|
return _fail("the drone arrived and the crafter is still at %d hp" % hp_after)
|
|
print("drone spent %d ore and put %d structure back" \
|
|
% [ore_before - game_state.building_resource(workshop_id, GameState.RESOURCE_ORE),
|
|
hp_after - hp_before])
|
|
|
|
# --- G. a destination that stops existing ---------------------------
|
|
game_state.building_inventory(workshop_id).set_amount(GameState.RESOURCE_ORE, 60)
|
|
var doomed: int = game_state.add_building(GameState.BUILDING_SHIP_LAB, Vector2(-900.0, 0.0))
|
|
game_state.damage_building(doomed, game_state.building_max_hp(GameState.BUILDING_SHIP_LAB) / 2)
|
|
game_state._advance_repairs(0.016)
|
|
var carrying: int = 0
|
|
for drone in game_state.repair_drones:
|
|
if String(drone.state) == GameState.DRONE_OUTBOUND and int(drone.target_id) == doomed:
|
|
carrying = int(drone.carrying)
|
|
if carrying <= 0:
|
|
return _fail("nothing was dispatched to the damaged lab")
|
|
var till_before: int = game_state.building_resource(workshop_id, GameState.RESOURCE_ORE)
|
|
game_state.destroy_building(doomed)
|
|
_simulate(30.0)
|
|
if game_state.building_resource(workshop_id, GameState.RESOURCE_ORE) != till_before + carrying:
|
|
return _fail("a drone whose target was levelled under it lost its %d ore" % carrying)
|
|
print("target levelled in flight: %d ore came home" % carrying)
|
|
|
|
# --- H. the upgrade reaches standing workshops ----------------------
|
|
var bonus_id: String = ""
|
|
for node in GameState.SKILL_TREE:
|
|
if int(node.get("workshop_bonus", 0)) > 0:
|
|
bonus_id = String(node.id)
|
|
if bonus_id == "":
|
|
return _fail("no skill node raises the drone count")
|
|
var before_count: int = _drones_of(workshop_id).size()
|
|
game_state.rocks = 5000
|
|
game_state.research_points = 20
|
|
if not game_state.unlock(bonus_id):
|
|
return _fail("could not research \"%s\"" % bonus_id)
|
|
if _drones_of(workshop_id).size() != before_count + 1:
|
|
return _fail("a workshop built before the upgrade still has %d drones" \
|
|
% _drones_of(workshop_id).size())
|
|
print("\"%s\" raises an already-standing workshop from %d to %d drones" \
|
|
% [bonus_id, before_count, _drones_of(workshop_id).size()])
|
|
|
|
print("TEST_SPACIAL_WORKSHOP: PASS")
|
|
quit(0)
|
|
return true
|