DISTORDER STACKING. Each space-time distorder vessel past the first raises the fleet's speed ceiling by 1.0, so a second one is worth crewing instead of being dead weight. GameState gains max_fleet_speed_multiplier, pushed by Fleet whenever composition changes - the same direction as every other fleet-derived figure, since GameState does not reach into the scene tree to count ships. The energy drain is normalised against the CURRENT ceiling, so full boost costs the same per second however wide it is: extra vessels buy more speed, not cheaper speed. Losing one re-clamps an over-ceiling boost rather than leaving the fleet travelling faster than its crew can sustain. TRANSFER MENU. Rows overlapped, and the last one ran into the hint text. row_height is divided from the available space, but each row's contents used a fixed pixel ladder - glyph at +10, buttons at +88, selector at +142 - which needs about 244px. Three resources fit; the fourth cut rows to 180 and every one spilled 64px into the next. Every offset is now a fraction of the row, so it cannot overflow. Also dropped the per-row AMOUNT caption (four copies of a word the footer hint already explains) and moved each glyph and name to the TOP of its row, where they read as its heading rather than the next row's. HANGAR. Six vessel types in one row gave each about 100px for a 155px label, so the names ran together. Now a 3x2 grid per section, sized from the space between the first heading and the status line, using the empty lower half. LAB RESEARCH REWARD. A harder enemy lab is worth more research points, and how hard a lab is depends on the layout authored for it - how many capture points, how many defenders - which the planet TYPE cannot know. PlanetSetup carries research_point_reward, set in the map editor, overriding the planet type's flat rate. 0 means "no opinion", so every map authored before this keeps its old value. RESEARCH LAB BACKGROUND. Its own shader instead of the crater field with a red tint, which read as "rocky planet, tinted" rather than someone's facility: plated floor, seams, service conduits, lit junctions. Loop-free by construction (#68's lesson applied up front) - conduits stay continuous across cells by keying a horizontal run to cell.y and a vertical to cell.x, so neighbouring cells agree without any cell scanning its neighbours. ENEMY DETAIL. Every enemy was a single flat outline, which left the artillery and the turret spawner as the same octagon at two sizes - the silhouette carried no information about what the thing did. EnemyDefinition gains detail_lines and detail_rings as DATA, so adding an enemy still means adding a definition rather than editing Enemy.gd, and the four non-default hulls are redrawn: a long-barrelled sniper, a squat mortar, a hive with launch bays, a capital ship with spine and ribs. Three things drew an enemy from its definition - the enemy, the map editor's markers, and the palette buttons - each reimplementing it from hull_points alone, so the new detail appeared in the game and nowhere else and an editor marker stopped matching what it placed. They now share EnemyDefinitions.draw_definition(). ARTILLERY. The 1.8s charge-up before the tower fired arrived long before the threat was legible and mostly read as the tower being stuck. It fires immediately now; the warning is the shell you can see in the air plus a small cross on the ground at the impact point. Deliberately not a blast-radius ring - a large translucent area was easy to read as the explosion having already happened, where the cross says "move off this spot". MAIN MENU. No Quit in any build (a web build has nothing to quit to, and Android's back gesture already does it), a plain black background, and the game is "Spaceship" - in the title and in config/name, which is what drives the browser tab for the web export. change_scene_to_file() returns an error and otherwise fails silently, leaving the menu up, which is indistinguishable from an unpressed button; it is now checked and reported on screen. Full suite green at 99/99. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
7 KiB
GDScript
152 lines
7 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()
|
|
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]:
|
|
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://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
|