246 lines
10 KiB
GDScript3
246 lines
10 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless test: run with
|
||
|
|
# godot --headless -s res://tests/test_planning_center.gd
|
||
|
|
# Forgejo #106 and #107 (#98a, #98b): the Planning Center.
|
||
|
|
#
|
||
|
|
# "let the player tell the system how many ships of a give type should
|
||
|
|
# always be available over the whole game (fleet + storage + outposts) ...
|
||
|
|
# the planning center automatically manage the ship craft building queues
|
||
|
|
# to ensure that the needed vessels are produced to fullfill the
|
||
|
|
# constraints."
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. the census counts a vessel exactly once wherever it is - flying,
|
||
|
|
# stored, stationed at an outpost, or in the air between any two
|
||
|
|
# B. a shortfall is queued at a crafter, and queued vessels count, so it
|
||
|
|
# does not re-order the same shortfall every tick
|
||
|
|
# C. it tops up rather than rewriting: an order placed by hand survives,
|
||
|
|
# and the planner appends beside it
|
||
|
|
# D. it stops when the target is met, and starts again when one is lost
|
||
|
|
# E. nothing happens without a planning centre, but the plan is remembered
|
||
|
|
# across losing and rebuilding one
|
||
|
|
# F. the panel drives the real targets through the real buttons
|
||
|
|
|
||
|
|
var game_state: Node
|
||
|
|
var scene: Node
|
||
|
|
var controller: Node
|
||
|
|
var menu: Control
|
||
|
|
var panel: Control
|
||
|
|
var crafter_id: int = 0
|
||
|
|
var outpost_id: int = 0
|
||
|
|
var stage: int = 0
|
||
|
|
var elapsed: float = 0.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.stored_vessels.clear()
|
||
|
|
game_state.vessel_deliveries.clear()
|
||
|
|
game_state.vessel_targets.clear()
|
||
|
|
game_state.fleet_vessel_types = PackedStringArray()
|
||
|
|
game_state.reset_progression()
|
||
|
|
game_state.reset_waves()
|
||
|
|
game_state.unlocked.append_array(["shipyards", "coordination", "forward_bases",
|
||
|
|
"vessel_science", "logistics_command"])
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_PLANNING_CENTER: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func _queued_at_crafter() -> int:
|
||
|
|
return game_state.pending_orders_of(crafter_id, GameState.VESSEL_INTERCEPTOR)
|
||
|
|
|
||
|
|
|
||
|
|
func _state_cases() -> bool:
|
||
|
|
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||
|
|
crafter_id = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0))
|
||
|
|
outpost_id = game_state.add_building(GameState.BUILDING_OUTPOST, Vector2(-900.0, 0.0))
|
||
|
|
var interceptor: String = GameState.VESSEL_INTERCEPTOR
|
||
|
|
|
||
|
|
# --- A. one vessel, one count, wherever it is ------------------------
|
||
|
|
if game_state.vessel_census(interceptor) != 0:
|
||
|
|
return _fail("an empty game counts %d vessels" % game_state.vessel_census(interceptor))
|
||
|
|
game_state.fleet_vessel_types = PackedStringArray([interceptor, interceptor])
|
||
|
|
game_state.stored_vessels.append(interceptor)
|
||
|
|
game_state.set_garrison_desired(outpost_id, interceptor, 1)
|
||
|
|
game_state.garrison_crew(outpost_id).append(interceptor)
|
||
|
|
game_state.vessel_deliveries.append({"type": interceptor, "from": Vector2.ZERO,
|
||
|
|
"to": Vector2(400.0, 0.0), "progress": 0.0, "to_outpost": 0})
|
||
|
|
if game_state.vessel_count_in_fleet(interceptor) != 2:
|
||
|
|
return _fail("the fleet reads %d" % game_state.vessel_count_in_fleet(interceptor))
|
||
|
|
if game_state.vessel_count_in_hangar(interceptor) != 1:
|
||
|
|
return _fail("the hangar reads %d" % game_state.vessel_count_in_hangar(interceptor))
|
||
|
|
if game_state.vessel_count_in_garrisons(interceptor) != 1:
|
||
|
|
return _fail("the garrisons read %d" % game_state.vessel_count_in_garrisons(interceptor))
|
||
|
|
if game_state.vessel_count_in_transit(interceptor) != 1:
|
||
|
|
return _fail("in transit reads %d" % game_state.vessel_count_in_transit(interceptor))
|
||
|
|
if game_state.vessel_census(interceptor) != 5:
|
||
|
|
return _fail("2 flying + 1 stored + 1 stationed + 1 moving counted as %d" \
|
||
|
|
% game_state.vessel_census(interceptor))
|
||
|
|
print("census: 2 flying, 1 stored, 1 stationed, 1 moving = %d" \
|
||
|
|
% game_state.vessel_census(interceptor))
|
||
|
|
|
||
|
|
# --- E (first half). no centre, no action ---------------------------
|
||
|
|
game_state.set_vessel_target(interceptor, 8)
|
||
|
|
for i in range(20):
|
||
|
|
game_state._advance_planning()
|
||
|
|
if _queued_at_crafter() != 0:
|
||
|
|
return _fail("orders were placed with no planning centre built")
|
||
|
|
print("with no planning centre, a standing order does nothing")
|
||
|
|
|
||
|
|
game_state.add_building(GameState.BUILDING_PLANNING_CENTER, Vector2(900.0, 0.0))
|
||
|
|
if not game_state.has_planning_center():
|
||
|
|
return _fail("the planning centre was built but is not recognised")
|
||
|
|
|
||
|
|
# --- B. the shortfall is queued, once ------------------------------
|
||
|
|
if game_state.vessel_shortfall(interceptor) != 3:
|
||
|
|
return _fail("a target of 8 against 5 reads a shortfall of %d" \
|
||
|
|
% game_state.vessel_shortfall(interceptor))
|
||
|
|
for i in range(20):
|
||
|
|
game_state._advance_planning()
|
||
|
|
if _queued_at_crafter() != 3:
|
||
|
|
return _fail("20 passes at a shortfall of 3 queued %d orders" % _queued_at_crafter())
|
||
|
|
if game_state.vessel_shortfall(interceptor) != 0:
|
||
|
|
return _fail("the shortfall is still %d with 3 on the slipway" \
|
||
|
|
% game_state.vessel_shortfall(interceptor))
|
||
|
|
print("a shortfall of 3 queued exactly 3, and 20 more passes added none")
|
||
|
|
|
||
|
|
# --- C. hand-placed orders are left alone ---------------------------
|
||
|
|
game_state.queue_order(crafter_id, GameState.VESSEL_SHIELD)
|
||
|
|
var by_hand: int = game_state.pending_orders_of(crafter_id, GameState.VESSEL_SHIELD)
|
||
|
|
game_state.set_vessel_target(interceptor, 10)
|
||
|
|
for i in range(20):
|
||
|
|
game_state._advance_planning()
|
||
|
|
if game_state.pending_orders_of(crafter_id, GameState.VESSEL_SHIELD) != by_hand:
|
||
|
|
return _fail("the planner cancelled a shield the player ordered by hand")
|
||
|
|
if _queued_at_crafter() != 5:
|
||
|
|
return _fail("raising the target to 10 left %d interceptors queued" % _queued_at_crafter())
|
||
|
|
print("a hand-placed shield survives; the planner appended beside it")
|
||
|
|
|
||
|
|
# --- D. it stops, and it reacts to a loss ---------------------------
|
||
|
|
game_state.set_vessel_target(interceptor, 5)
|
||
|
|
var queued_before: int = _queued_at_crafter()
|
||
|
|
for i in range(20):
|
||
|
|
game_state._advance_planning()
|
||
|
|
if _queued_at_crafter() != queued_before:
|
||
|
|
return _fail("lowering the target changed the queue by %d" \
|
||
|
|
% (_queued_at_crafter() - queued_before))
|
||
|
|
# Losing one from the fleet is a real shortfall again.
|
||
|
|
game_state.set_vessel_target(interceptor, 12)
|
||
|
|
var before_loss: int = _queued_at_crafter()
|
||
|
|
for i in range(20):
|
||
|
|
game_state._advance_planning()
|
||
|
|
var after_top_up: int = _queued_at_crafter()
|
||
|
|
game_state.fleet_vessel_types = PackedStringArray([interceptor])
|
||
|
|
for i in range(20):
|
||
|
|
game_state._advance_planning()
|
||
|
|
if _queued_at_crafter() <= after_top_up:
|
||
|
|
return _fail("losing a vessel did not put a replacement on the slipway")
|
||
|
|
print("target met -> nothing queued; a vessel lost -> %d queued (was %d)" \
|
||
|
|
% [_queued_at_crafter() - after_top_up, before_loss])
|
||
|
|
|
||
|
|
# --- E (second half). the plan outlives the building ----------------
|
||
|
|
for b in game_state.buildings:
|
||
|
|
if game_state.building_has_capability(b, "planning"):
|
||
|
|
game_state.destroy_building(int(b.id))
|
||
|
|
break
|
||
|
|
if game_state.has_planning_center():
|
||
|
|
return _fail("the planning centre survived being destroyed")
|
||
|
|
if game_state.vessel_target(interceptor) != 12:
|
||
|
|
return _fail("losing the building erased the plan")
|
||
|
|
var frozen: int = _queued_at_crafter()
|
||
|
|
game_state.fleet_vessel_types = PackedStringArray()
|
||
|
|
for i in range(20):
|
||
|
|
game_state._advance_planning()
|
||
|
|
if _queued_at_crafter() != frozen:
|
||
|
|
return _fail("the planner kept working after its building was destroyed")
|
||
|
|
print("destroyed: the plan is remembered, and nothing acts on it")
|
||
|
|
return false
|
||
|
|
|
||
|
|
|
||
|
|
func _process(delta: float) -> bool:
|
||
|
|
elapsed += delta
|
||
|
|
match stage:
|
||
|
|
0:
|
||
|
|
if _state_cases():
|
||
|
|
return true
|
||
|
|
game_state.buildings.clear()
|
||
|
|
game_state.vessel_targets.clear()
|
||
|
|
scene = load("res://world/Main.tscn").instantiate()
|
||
|
|
root.add_child(scene)
|
||
|
|
controller = scene.get_node("BuildController")
|
||
|
|
menu = scene.get_node("UI/PlanningMenu")
|
||
|
|
panel = scene.get_node("UI/PlanningMenu/Panel")
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 1
|
||
|
|
return false
|
||
|
|
1:
|
||
|
|
if elapsed < 0.3:
|
||
|
|
return false
|
||
|
|
scene.get_node("AsteroidField").set_process(false)
|
||
|
|
scene.get_node("EnemySpawner").set_process(false)
|
||
|
|
scene.get_node("BuildingField").place(GameState.BUILDING_PLANNING_CENTER,
|
||
|
|
Vector2(900.0, 0.0))
|
||
|
|
elapsed = 0.0
|
||
|
|
stage = 2
|
||
|
|
return false
|
||
|
|
2:
|
||
|
|
if elapsed < 0.2:
|
||
|
|
return false
|
||
|
|
# --- F. the real panel, through the real buttons -------------
|
||
|
|
var node: Node2D = null
|
||
|
|
for b in scene.get_node("BuildingField").spawned:
|
||
|
|
if is_instance_valid(b) and b.building_type == GameState.BUILDING_PLANNING_CENTER:
|
||
|
|
node = b
|
||
|
|
if node == null:
|
||
|
|
return _fail("no planning centre node was placed")
|
||
|
|
if node.primary_action() != "planning":
|
||
|
|
return _fail("its primary action is \"%s\"" % node.primary_action())
|
||
|
|
controller.current_building = node
|
||
|
|
controller._on_action_pressed()
|
||
|
|
if not menu.visible:
|
||
|
|
return _fail("the planning centre did not open its menu")
|
||
|
|
|
||
|
|
var row: Dictionary = {}
|
||
|
|
for candidate in panel._rows:
|
||
|
|
if String(candidate.type) == GameState.VESSEL_INTERCEPTOR:
|
||
|
|
row = candidate
|
||
|
|
if row.is_empty():
|
||
|
|
return _fail("the panel has no row for the interceptor")
|
||
|
|
row.plus.pressed.emit()
|
||
|
|
row.plus.pressed.emit()
|
||
|
|
row.plus.pressed.emit()
|
||
|
|
if game_state.vessel_target(GameState.VESSEL_INTERCEPTOR) != 3:
|
||
|
|
return _fail("three presses of + set a target of %d" \
|
||
|
|
% game_state.vessel_target(GameState.VESSEL_INTERCEPTOR))
|
||
|
|
row.minus.pressed.emit()
|
||
|
|
if game_state.vessel_target(GameState.VESSEL_INTERCEPTOR) != 2:
|
||
|
|
return _fail("- did not lower the target")
|
||
|
|
if not row.count.text.ends_with("/2"):
|
||
|
|
return _fail("the row reads \"%s\" - it should show have over want" % row.count.text)
|
||
|
|
# The note is what makes an unmet plan legible: it says where the
|
||
|
|
# vessels actually are.
|
||
|
|
if row.note.text.strip_edges() == "" and game_state.vessel_census(
|
||
|
|
GameState.VESSEL_INTERCEPTOR) > 0:
|
||
|
|
return _fail("vessels exist but the row says nothing about where they are")
|
||
|
|
print("the panel drives the real plan: + + + - leaves \"%s\" (%s)" \
|
||
|
|
% [row.count.text, row.note.text])
|
||
|
|
|
||
|
|
var status: String = scene.get_node("UI/PlanningMenu/StatusLabel").text
|
||
|
|
if status.strip_edges() == "":
|
||
|
|
return _fail("the planning menu reports nothing at all")
|
||
|
|
print("status: \"%s\"" % status)
|
||
|
|
|
||
|
|
print("TEST_PLANNING_CENTER: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
return false
|