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. 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