spaceship/tests/test_dev_mode.gd
2026-09-03 10:33:05 +02:00

156 lines
7.3 KiB
GDScript

extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_dev_mode.gd
# A "Dev Mode" start for play-testing: one of every planet type next to the
# spawn, every resource banked, and research points to spend. Offered by the
# main menu only when the game is run from Godot, alongside the map editor.
#
# The same change took the training wheels off a NORMAL game: no 500 free
# ore, and no gas giant guaranteed in the origin chunk. Both of those were
# play-testing conveniences that had become part of the real game, so this
# test covers both sides - what dev mode hands you, and what a normal game
# no longer does.
#
# Covers:
# A. a dev game banks every resource type and research points
# B. a dev game is a normal game PLUS that, not a separate definition
# C. one of each planet type sits within a chunk or two of the spawn
# D. none of that leaks into a normal game, and starting one clears it
# E. a normal game starts with an empty bank
# F. the menu gates dev mode and the map editor on the same rule
var game_state: Node
var menu: Node
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
# --- E. a normal game hands over nothing ----------------------
game_state.reset_new_game()
# #108: reset_new_game() also arms the prologue. Nothing below is
# about it, and this test never builds a scene for it to run in.
game_state.intro_active = false
if game_state.rocks != 0:
return _fail("a normal game started with %d ore banked" % game_state.rocks)
for resource_type in game_state.resource_types():
if game_state.banked_resource(resource_type) != 0:
return _fail("a normal game started with %s banked" % resource_type)
if game_state.research_points != 0:
return _fail("a normal game started with research points")
if game_state.dev_mode:
return _fail("a normal game started in dev mode")
print("normal game starts with an empty bank and no research points")
# --- D. no dev planets in a normal game -----------------------
# The dev arrangement must not survive into a normal world. Noise
# could coincidentally place a planet in one of these chunks, so
# what is asserted is that the ARRANGEMENT is not reproduced, not
# that each chunk is empty.
var reproduced: bool = true
for chunk in WorldGen.DEV_PLANET_CHUNKS:
var expected: String = String(WorldGen.DEV_PLANET_CHUNKS[chunk])
var data: Dictionary = WorldGen.planet_data_at_chunk(chunk)
if not WorldGen.has_planet_at_chunk(chunk) or String(data.get("definition_id", "")) != expected:
reproduced = false
if Vector2(data.get("local_pos", Vector2.ZERO)) == WorldGen.DEV_PLANET_LOCAL_POS:
return _fail("a normal game placed a planet at dev mode's fixed in-chunk position, in chunk %s" % chunk)
if reproduced:
return _fail("a normal game reproduced dev mode's whole planet arrangement")
print("normal game does not carry dev mode's planets")
# --- A + B. what a dev game hands over ------------------------
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
game_state.planet_capture["chunk:9:9"] = {"captured": [true], "conquered": true}
game_state.start_dev_game()
if not game_state.dev_mode:
return _fail("start_dev_game() did not set dev_mode")
for resource_type in game_state.resource_types():
var banked: int = game_state.banked_resource(resource_type)
if banked != GameState.DEV_RESOURCE_AMOUNT:
return _fail("dev game banked %d of %s, expected %d" \
% [banked, resource_type, GameState.DEV_RESOURCE_AMOUNT])
if game_state.research_points != GameState.DEV_RESEARCH_POINTS:
return _fail("dev game granted %d research points, expected %d" \
% [game_state.research_points, GameState.DEV_RESEARCH_POINTS])
# B: it still went through reset_new_game, so a dirty run is gone.
if not game_state.buildings.is_empty() or not game_state.planet_capture.is_empty():
return _fail("a dev game did not reset the previous run - it must be a fresh game plus a stock")
print("dev game banks %d of each resource and %d research points, from a clean slate" \
% [GameState.DEV_RESOURCE_AMOUNT, GameState.DEV_RESEARCH_POINTS])
# --- C. one of each planet type, close by ---------------------
var found: Dictionary = {}
for chunk in WorldGen.DEV_PLANET_CHUNKS:
if not WorldGen.has_planet_at_chunk(chunk):
return _fail("dev mode chunk %s holds no planet" % chunk)
var data: Dictionary = WorldGen.planet_data_at_chunk(chunk)
var definition_id: String = String(data.get("definition_id", ""))
if definition_id != String(WorldGen.DEV_PLANET_CHUNKS[chunk]):
return _fail("dev mode chunk %s holds a %s, expected a %s" \
% [chunk, definition_id, WorldGen.DEV_PLANET_CHUNKS[chunk]])
found[definition_id] = true
# "Near the start" is the whole point - a play-tester should
# reach any of them in seconds, not go exploring.
var world_pos: Vector2 = WorldGen.chunk_origin(chunk) + Vector2(data.local_pos)
if world_pos.length() > WorldGen.CHUNK_SIZE * 2.5:
return _fail("dev mode's %s spawned %.0f units away, too far to be 'near the start'" \
% [definition_id, world_pos.length()])
for definition_id in [PlanetDefinitions.ROCKY_ID, PlanetDefinitions.GAS_GIANT_ID,
PlanetDefinitions.BLACK_HOLE_ID, PlanetDefinitions.RESEARCH_LAB_ID,
PlanetDefinitions.BOSS_ID]:
if not found.has(definition_id):
return _fail("dev mode is missing a %s - it must plant one of EVERY planet type" % definition_id)
print("dev mode plants all %d planet types within %.0f units of the spawn" \
% [found.size(), WorldGen.CHUNK_SIZE * 2.5])
# --- D. starting a normal game clears it ----------------------
game_state.reset_new_game()
if game_state.dev_mode:
return _fail("starting a normal game after a dev one left dev_mode set")
if game_state.rocks != 0 or game_state.research_points != 0:
return _fail("starting a normal game after a dev one kept the dev stock")
print("starting a normal game afterwards clears dev mode entirely")
menu = load("res://ui/MainMenu.tscn").instantiate()
root.add_child(menu)
current_scene = menu
elapsed = 0.0
stage = 1
return false
1:
# --- F. the menu gates both on the same rule ------------------
if elapsed < 0.1:
return false
var developing: bool = menu.is_developer_build()
if menu.dev_button.visible != developing:
return _fail("the dev mode button's visibility did not follow is_developer_build()")
if menu.editor_button.visible != developing:
return _fail("the map editor button's visibility did not follow is_developer_build()")
if not menu.play_button.visible:
return _fail("gating the development entries also hid Play")
# Hiding is presentation; pressing it anyway must still be refused.
if not developing:
menu._on_dev_mode_pressed()
if game_state.dev_mode:
return _fail("dev mode started outside a developer build")
print("dev mode and the map editor are both gated on is_developer_build() (%s here)" % developing)
print("TEST_DEV_MODE: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_DEV_MODE: FAIL (%s)" % message)
quit(1)
return true