Compare commits

..
22 changed files with 39 additions and 809 deletions

View file

@ -197,8 +197,6 @@ var context_actions_expanded: bool = false
# Gap between the last resource row and the first row of HUD buttons.
const HUD_COLUMN_MARGIN: float = 18.0
# Vertical gap between one row of HUD buttons and the next.
const HUD_ROW_GAP: float = 16.0
# Seats the top-anchored HUD button columns below the resource readout instead
@ -440,17 +438,8 @@ func _process(delta: float) -> void:
func _layout_context_buttons() -> void:
# Build is permanently under the resource counter. The contextual berth
# directly below it is compact until it has more than one real choice.
#
# "Directly below it" is MEASURED from the build button rather than
# written down as a y. This used to hardcode 324, which was the right
# answer only while the build button sat at the 160 the scene authored -
# once _seat_hud_columns() started moving the whole block down to clear
# the resource readout, this ran on the next context change and dropped
# the action row straight back on top of the build button.
var row_top: float = build_button.position.y + build_button.size.y + HUD_ROW_GAP
var row_height: float = build_button.size.y
action_button.offset_top = row_top
action_button.offset_bottom = row_top + row_height
action_button.offset_top = 324
action_button.offset_bottom = 472
var buttons: Array = _context_action_buttons()
if not context_actions_expanded:
for button in buttons:
@ -464,8 +453,8 @@ func _layout_context_buttons() -> void:
button.visible = true
button.offset_left = 188 + i * 164
button.offset_right = button.offset_left + 148
button.offset_top = row_top
button.offset_bottom = row_top + row_height
button.offset_top = 324
button.offset_bottom = 472
func _context_action_buttons() -> Array:

View file

@ -5,10 +5,6 @@ const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd")
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd")
const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd")
# By path, not by its `WorldGen` global name: an autoload compiles before the
# global class table is necessarily populated, the same hazard PlanetSetups.gd
# works around.
const WORLD_GEN = preload("res://WorldGen.gd")
# Autoload singleton - the one thing that survives change_scene_to_file()
@ -18,7 +14,7 @@ const WORLD_GEN = preload("res://WorldGen.gd")
# survive the trip has to live here instead.
var has_return_position: bool = false
var surviving_ship_count: int = STARTING_SHIP_COUNT
var surviving_ship_count: int = 5
var surviving_vessel_types: PackedStringArray = PackedStringArray()
var return_world_position: Vector2 = Vector2.ZERO
var return_camera_zoom: float = 1.0
@ -56,20 +52,6 @@ func set_fleet_speed_multiplier(value: float) -> void:
signal research_points_changed(total: int)
var research_points: int = 0
# Set only by start_dev_game(), which the main menu offers only when running
# from Godot. Makes WorldGen plant one of every planet type around the spawn.
# Cleared by reset_new_game(), so starting a normal game after a dev one
# cannot leave a dev world behind.
#
# The setter mirrors it into WorldGen, which cannot read it back: world
# generation is static and dependency-free by design. This stays the single
# place to set it - assigning dev_mode anywhere, tests included, reaches
# world generation.
var dev_mode: bool = false:
set(value):
dev_mode = value
WORLD_GEN.dev_planets_enabled = value
# Explored-chunk history for the world map's fog-of-war reveal. Lives
# here (not on ExplorationTracker, which gets destroyed and recreated on
# every scene swap) so exploration progress isn't lost every time the
@ -86,11 +68,7 @@ const RESOURCE_TYPES: PackedStringArray = RESOURCE_DEFINITIONS.ORDER
# All resource-bearing locations use the same container. The old public
# names below are computed compatibility accessors, not separate storage.
# Starts EMPTY. A new game used to open with 500 ore banked, which quietly
# skipped the opening of the game - the first mining run is what teaches the
# fleet hold, the return trip and the mothership. Dev mode (start_dev_game)
# is where a stocked start belongs.
var bank_inventory: RefCounted = INVENTORY.new()
var bank_inventory: RefCounted = INVENTORY.new({RESOURCE_ORE: 500})
var fleet_inventory: RefCounted = INVENTORY.new()
var rocks: int:
@ -346,11 +324,7 @@ const SKILL_TREE: Array = [
"desc": "Build gas burners. Each converts gas into energy at its own site - hub transporters keep it stocked and collect what it makes.",
},
{
# Cheaper than it was (30 ore / 1 point): the fleet now starts at
# BASE_FLEET_SIZE 3 rather than 5, so the first two doctrines are no
# longer a mid-game luxury but the ordinary way out of the opening.
# Pricing them as a luxury made the early game feel stuck.
"id": "wing", "name": "Wing Doctrine", "cost": 15, "requires": "coordination",
"id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination",
"x": 0.82, "fleet_bonus": 1, "research_points": 1,
"desc": "Permanent: +1 vessel in the fleet.",
},
@ -361,41 +335,18 @@ const SKILL_TREE: Array = [
# prerequisite, also x=0.82) instead, so the whole fleet-size
# doctrine branch now runs straight rather than zigzagging into
# someone else's lane.
# Also cut (70 ore / 2 points), for the same reason as wing.
"id": "squadron", "name": "Squadron Doctrine", "cost": 35, "requires": "wing",
"x": 0.82, "fleet_bonus": 1, "research_points": 1,
"desc": "Permanent: +1 vessel in the fleet.",
},
{
"id": "armada", "name": "Armada Doctrine", "cost": 90, "requires": "squadron",
"id": "squadron", "name": "Squadron Doctrine", "cost": 70, "requires": "wing",
"x": 0.82, "fleet_bonus": 1, "research_points": 2,
"desc": "Permanent: +1 vessel in the fleet.",
},
{
# The last two tiers grant +2 rather than +1. Seven single-vessel
# nodes in one lane would be a long, monotonous chain for a branch
# that only ever does one thing; two larger, costlier steps carry the
# ceiling from 6 to 10 while keeping the branch a readable length.
"id": "battlegroup", "name": "Battlegroup Doctrine", "cost": 180, "requires": "armada",
"x": 0.82, "fleet_bonus": 2, "research_points": 4,
"desc": "Permanent: +2 vessels in the fleet.",
},
{
"id": "grand_fleet", "name": "Grand Fleet Doctrine", "cost": 320, "requires": "battlegroup",
"x": 0.82, "fleet_bonus": 2, "research_points": 6,
"desc": "Permanent: +2 vessels in the fleet.",
"id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron",
"x": 0.82, "fleet_bonus": 1, "research_points": 3,
"desc": "Permanent: +1 vessel in the fleet.",
},
]
# How many SLOTS the fleet has before any doctrine is researched. Distinct
# from STARTING_SHIP_COUNT below: this is capacity, that is how many vessels
# are actually flying. The two used to be one constant, which forced a new
# game to open with a full fleet and left the first crafter with nothing to
# do - there was no room to put a vessel until a doctrine was researched.
const BASE_FLEET_SIZE: int = 3
# A new game opens with a SINGLE vessel. Building the fleet up is the early
# game, so the two empty slots are the first thing a ship crafter is for.
const STARTING_SHIP_COUNT: int = 1
const BASE_FLEET_SIZE: int = 5
var unlocked: Array = []
@ -497,7 +448,7 @@ func reset_progression() -> void:
# fresh game.
func reset_new_game() -> void:
has_return_position = false
surviving_ship_count = STARTING_SHIP_COUNT
surviving_ship_count = BASE_FLEET_SIZE
surviving_vessel_types = PackedStringArray()
return_world_position = Vector2.ZERO
return_camera_zoom = 1.0
@ -508,9 +459,8 @@ func reset_new_game() -> void:
_distorder_drain_progress = 0.0
boost_depleted_notice = false
research_points = 0
dev_mode = false
explored_chunks.clear()
bank_inventory = INVENTORY.new()
bank_inventory = INVENTORY.new({RESOURCE_ORE: 500})
fleet_inventory = INVENTORY.new()
buildings.clear()
next_building_id = 1
@ -523,29 +473,6 @@ func reset_new_game() -> void:
reset_formation()
# Amounts a dev game opens with: enough of everything to reach any part of
# the game immediately, without being so large that costs stop registering.
const DEV_RESOURCE_AMOUNT: int = 2000
const DEV_RESEARCH_POINTS: int = 500
# A new game stocked for play-testing: every resource banked, research points
# to spend, and one of each planet type next to the spawn (WorldGen reads
# dev_mode for that). Offered by the main menu only when running from Godot -
# see MainMenu.gd - so it cannot be reached in a shipped build.
#
# Deliberately built ON TOP of reset_new_game() rather than beside it, so a
# dev game is a normal game plus a stock rather than a second, separately
# maintained definition of what a fresh game is.
func start_dev_game() -> void:
reset_new_game()
dev_mode = true
for resource_type in resource_types():
bank_inventory.set_amount(resource_type, DEV_RESOURCE_AMOUNT)
research_points = DEV_RESEARCH_POINTS
research_points_changed.emit(research_points)
# --- building stock and production queue (#16) -----------------------------
# Since #12, a building cannot just deduct from the bank at the moment of
@ -1174,13 +1101,9 @@ func get_mothership_position() -> Variant:
# capacity upgrade is researched, but they are defined up front so the wedge
# keeps a deliberate shape as it grows rather than having positions invented
# at unlock time.
# Ten slots, because max_fleet_size() caps itself on this array's length -
# so the wedge has to be long enough for every doctrine to be worth buying.
# The last pair continues the V rather than starting a new shape.
const DEFAULT_FORMATION: Array = [
Vector2(0, -48), Vector2(-30, -8), Vector2(30, -8), Vector2(-60, 32), Vector2(60, 32),
Vector2(-90, 72), Vector2(90, 72), Vector2(0, 56),
Vector2(-120, 112), Vector2(120, 112),
]
var formation: Array = DEFAULT_FORMATION.duplicate()

View file

@ -1,35 +1,8 @@
extends Node2D
# The menu stacks whatever entries are actually available, rather than
# trusting the positions authored in the scene.
#
# Two of the four entries only exist when the game is run from Godot, so a
# shipped build shows a two-entry menu and an editor run shows four. Fixed
# offsets would leave holes where the hidden entries used to be - the same
# failure the HUD's button columns had, where a literal y meant "below the
# thing above me" and stopped being true when that thing moved. Here the
# stack is measured instead, and centred on whatever it ends up containing.
const ROW_BUTTON_HEIGHT: float = 128.0
const ROW_LABEL_OFFSET: float = 134.0
const ROW_LABEL_HEIGHT: float = 56.0
const ROW_HEIGHT: float = ROW_LABEL_OFFSET + ROW_LABEL_HEIGHT
const ROW_GAP: float = 20.0
const ROW_PITCH: float = ROW_HEIGHT + ROW_GAP
# The stack is centred on this rather than on the screen, to leave the title
# above it room to breathe.
const STACK_CENTER_Y: float = 40.0
const STATUS_GAP: float = 20.0
const STATUS_HEIGHT: float = 70.0
@onready var play_button: IconButton = $UI/PlayButton
@onready var play_label: Label = $UI/PlayLabel
@onready var editor_button: IconButton = $UI/EditorButton
@onready var editor_label: Label = $UI/EditorLabel
@onready var dev_button: IconButton = $UI/DevModeButton
@onready var dev_label: Label = $UI/DevModeLabel
@onready var quit_button: IconButton = $UI/QuitButton
@onready var quit_label: Label = $UI/QuitLabel
@onready var status_label: Label = $UI/StatusLabel
@ -37,78 +10,16 @@ func _ready() -> void:
play_button.pressed.connect(_on_play_pressed)
editor_button.pressed.connect(_on_editor_pressed)
editor_button.enabled = true
dev_button.pressed.connect(_on_dev_mode_pressed)
dev_button.enabled = true
quit_button.pressed.connect(_on_quit_pressed)
quit_button.visible = not OS.has_feature("web")
# Dev mode and the map editor are development tools, not features of the
# game: they are for building and play-testing it, and a player who
# installed a build has no use for either. `editor` is the feature tag
# Godot sets when running from the editor and clears in an export, which
# is exactly the "started from Godot" distinction wanted here.
var developing: bool = is_developer_build()
editor_button.visible = developing
editor_label.visible = developing
dev_button.visible = developing
dev_label.visible = developing
_layout_stack()
# Wrapped so tests can state the rule they are checking rather than
# duplicating the feature-tag string.
func is_developer_build() -> bool:
return OS.has_feature("editor")
func _layout_stack() -> void:
var rows: Array = []
for row in [[play_button, play_label], [editor_button, editor_label],
[dev_button, dev_label], [quit_button, quit_label]]:
if row[0].visible:
rows.append(row)
if rows.is_empty():
return
# The last row carries no trailing gap, so the block is one pitch per row
# minus that gap.
var block_height: float = float(rows.size()) * ROW_PITCH - ROW_GAP
var top: float = STACK_CENTER_Y - block_height * 0.5
for row in rows:
var button: Control = row[0]
var label: Label = row[1]
button.offset_top = top
button.offset_bottom = top + ROW_BUTTON_HEIGHT
label.offset_top = top + ROW_LABEL_OFFSET
label.offset_bottom = top + ROW_LABEL_OFFSET + ROW_LABEL_HEIGHT
top += ROW_PITCH
# Status sits under the whole stack, where a message about any entry can
# be read without it shifting the entries themselves.
var status_top: float = top - ROW_GAP + STATUS_GAP
status_label.offset_top = status_top
status_label.offset_bottom = status_top + STATUS_HEIGHT
func _on_play_pressed() -> void:
GameState.reset_new_game()
get_tree().change_scene_to_file("res://Main.tscn")
func _on_dev_mode_pressed() -> void:
# Guarded as well as hidden. Visibility is presentation; this is the rule.
if not is_developer_build():
status_label.text = "Dev mode only runs from Godot"
return
GameState.start_dev_game()
get_tree().change_scene_to_file("res://Main.tscn")
func _on_editor_pressed() -> void:
if not is_developer_build():
status_label.text = "Map editor only runs from Godot"
return
if editor_button.enabled:
get_tree().change_scene_to_file("res://PlanetSetupEditor.tscn")
return

View file

@ -97,33 +97,6 @@ theme_override_font_sizes/font_size = 32
horizontal_alignment = 1
text = "Map Editor"
[node name="DevModeButton" type="Control" parent="UI"]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -64
offset_right = 64
offset_top = 190
offset_bottom = 318
script = ExtResource("2")
icon = 29
glow_color = Color(1, 0.7, 0.25, 1)
[node name="DevModeLabel" type="Label" parent="UI"]
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -200
offset_right = 200
offset_top = 324
offset_bottom = 380
theme_override_font_sizes/font_size = 32
horizontal_alignment = 1
text = "Dev Mode"
[node name="StatusLabel" type="Label" parent="UI"]
anchor_left = 0.5
anchor_right = 0.5

View file

@ -40,12 +40,7 @@ func _ready() -> void:
camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom)
GameState.has_return_position = false
else:
# A brand new game. STARTING_SHIP_COUNT rather than a literal: this
# said 5, which was the old BASE_FLEET_SIZE written out by hand, and
# it silently became "however many slots exist" once max_fleet_size()
# clamped it. The opening fleet size is one decision and it lives in
# GameState.
fleet.spawn_ships(GameState.STARTING_SHIP_COUNT, Vector2.ZERO)
fleet.spawn_ships(5, Vector2.ZERO)
# AsteroidField/PlanetField normally only populate nearby chunks once
# their own check_interval timer elapses (~1s) - fine during normal

View file

@ -29,42 +29,12 @@ const BLACK_HOLE_CHANCE: float = 0.05
# research points had become to progression.
const RESEARCH_LAB_CHANCE: float = 0.16
# DEV MODE ONLY: one of every planet type, parked in the chunks immediately
# around the spawn so all four can be reached in seconds.
#
# This replaces the guaranteed gas giant that used to sit in the origin
# chunk for every game. That was a play-testing convenience which had become
# part of the real world's shape - every player got the same landmark in the
# same place, and the surrounding space was generated as if it were normal.
# Play-testing wants ALL the planet types to hand, not just gas, and a
# normal game wants none of them handed over; those are different needs and
# they now have different mechanisms.
#
# The local positions keep each planet off the spawn point itself: a fresh
# fleet starts at world origin, which is the top-left corner of chunk (0,0).
const DEV_PLANET_CHUNKS: Dictionary = {
Vector2i(0, 0): PLANET_DEFINITIONS.GAS_GIANT_ID,
Vector2i(1, 0): PLANET_DEFINITIONS.ROCKY_ID,
Vector2i(0, 1): PLANET_DEFINITIONS.RESEARCH_LAB_ID,
Vector2i(1, 1): PLANET_DEFINITIONS.BLACK_HOLE_ID,
}
const DEV_PLANET_LOCAL_POS: Vector2 = Vector2(600.0, 600.0)
# Dev mode plants a fixed set of planets; a normal game must not see them.
#
# PUSHED here by GameState.dev_mode's setter rather than read back out of the
# autoload. World generation is deliberately dependency-free - it is static,
# seeded purely by chunk coordinates, and callable from anywhere - and an
# autoload cannot be named from here anyway: `GameState` is not resolvable as
# a global identifier this early, the same load-order hazard PlanetSetups.gd
# and PlanetSurfaceController.gd already work around. GameState.dev_mode
# stays the single source of truth; this is its mirror.
static var dev_planets_enabled: bool = false
static func _dev_planets_active() -> bool:
return dev_planets_enabled
# A predictable early gas source for play-testing. The origin chunk is
# deterministically normal space. Its explicit in-chunk position puts it
# roughly 1,080 world units from a fresh fleet: easy to reach, but not
# sitting on the spawn.
const NEARBY_GAS_GIANT_CHUNK: Vector2i = Vector2i.ZERO
const NEARBY_GAS_GIANT_LOCAL_POS: Vector2 = Vector2(900.0, 600.0)
static var _noise: FastNoiseLite
static var _planet_noise: FastNoiseLite
@ -106,7 +76,7 @@ static func biome_at_chunk(chunk: Vector2i) -> int:
static func has_planet_at_chunk(chunk: Vector2i) -> bool:
if _dev_planets_active() and DEV_PLANET_CHUNKS.has(chunk):
if chunk == NEARBY_GAS_GIANT_CHUNK:
return true
var n: float = _get_planet_noise().get_noise_2d(float(chunk.x), float(chunk.y))
return (biome_at_chunk(chunk) == Biome.ASTEROID_FIELD and n > PLANET_THRESHOLD) or (biome_at_chunk(chunk) == Biome.SPACE and n > GAS_GIANT_THRESHOLD)
@ -122,9 +92,8 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary:
# can never drift out of sync with each other.
var rng: RandomNumberGenerator = RandomNumberGenerator.new()
rng.seed = hash(chunk)
var forced_definition_id: String = String(DEV_PLANET_CHUNKS.get(chunk, "")) \
if _dev_planets_active() else ""
var local_pos: Vector2 = DEV_PLANET_LOCAL_POS if not forced_definition_id.is_empty() \
var guaranteed_nearby_giant: bool = chunk == NEARBY_GAS_GIANT_CHUNK
var local_pos: Vector2 = NEARBY_GAS_GIANT_LOCAL_POS if guaranteed_nearby_giant \
else Vector2(rng.randf() * CHUNK_SIZE, rng.randf() * CHUNK_SIZE)
var shape_seed: int = rng.randi()
# Drawn unconditionally, in the same fixed position every call,
@ -136,20 +105,15 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary:
var research_lab_roll: float = rng.randf()
# A stable per-chunk roll makes gas giants persistent landmarks rather
# than a different resource whenever the scene reloads.
var gas_giant: bool = biome_at_chunk(chunk) == Biome.SPACE
var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE
var definition_id: String = PLANET_DEFINITIONS.GAS_GIANT_ID if gas_giant else PLANET_DEFINITIONS.ROCKY_ID
# A rare third outcome, seeded the same deterministic way as everything
# else here.
if black_hole_roll < BLACK_HOLE_CHANCE:
# else here. Never overrides the guaranteed early gas giant landmark -
# that chunk's whole purpose is to always be the same reachable source.
if not guaranteed_nearby_giant and black_hole_roll < BLACK_HOLE_CHANCE:
definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID
elif research_lab_roll < RESEARCH_LAB_CHANCE:
elif not guaranteed_nearby_giant and research_lab_roll < RESEARCH_LAB_CHANCE:
definition_id = PLANET_DEFINITIONS.RESEARCH_LAB_ID
# Dev mode's fixed planting wins over every roll above. Applied last, and
# AFTER all the rng draws, so switching it on cannot shift which random
# values any other chunk consumes - a dev world and a normal one differ
# only in these four chunks.
if not forced_definition_id.is_empty():
definition_id = forced_definition_id
var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id)
var names: PackedStringArray = definition.names
var planet_name: String = names[rng.randi() % names.size()]

View file

@ -1,152 +0,0 @@
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 menu.play_button.visible != true or menu.quit_button.visible != (not OS.has_feature("web")):
return _fail("gating the development entries also hid an entry a player needs")
# 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

View file

@ -32,13 +32,6 @@ func _physics_process(delta: float) -> bool:
# happens strictly after _ready(). Here, at the point _ready() is
# already confirmed complete, it's safe.
main_scene.get_node("EnemySpawner").set_process(false)
# Crew the fleet HERE, not in _initialize(), for the same reason the
# set_process(false) call above lives here: work done in _initialize()
# can be undone by the engine's deferred tree-entry finalization. A
# new game flies one vessel into three slots now, and this test needs
# a full one.
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
fleet.spawn_ships(5, Vector2.ZERO)
target_ship = fleet.ships[0]
var enemy_scene: PackedScene = load("res://Enemy.tscn")
enemy = enemy_scene.instantiate()

View file

@ -32,13 +32,6 @@ func _physics_process(delta: float) -> bool:
# happens strictly after _ready(). Here, at the point _ready() is
# already confirmed complete, it's safe.
main_scene.get_node("EnemySpawner").set_process(false)
# Crew the fleet HERE, not in _initialize(), for the same reason the
# set_process(false) call above lives here: work done in _initialize()
# can be undone by the engine's deferred tree-entry finalization. A
# new game flies one vessel into three slots now, and this test needs
# a full one.
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
fleet.spawn_ships(5, Vector2.ZERO)
var enemy_scene: PackedScene = load("res://Enemy.tscn")
enemy = enemy_scene.instantiate()
enemy.position = fleet.get_centroid() + SPAWN_OFFSET

View file

@ -54,9 +54,6 @@ var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
beam_script = load("res://EnemyBigLaserBeam.gd")
# Four vessels are crewed below, past BASE_FLEET_SIZE's 3 slots, so the
# capacity doctrines are granted rather than assumed.
game_state.unlocked.append_array(["wing", "squadron", "armada"])
main = load("res://Main.tscn").instantiate()
root.add_child(main)
fleet = main.get_node("Fleet")

View file

@ -1,139 +0,0 @@
extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_fleet_growth.gd
# A new game opens with a SINGLE vessel and three slots, and the fleet grows
# from there through the doctrine branch of the skill tree.
#
# BASE_FLEET_SIZE used to be both "how many slots exist" and "how many ships
# you start with", which forced a new game to open with a full fleet - there
# was nowhere to put a vessel until a doctrine was researched, so the first
# ship crafter had nothing to do. The two are now separate constants, and
# this test pins the distinction.
#
# Covers:
# A. a new game starts with one vessel and three slots
# B. the fleet really spawns one ship, and can be grown into its slots
# C. every doctrine raises the ceiling by its own stated bonus
# D. the doctrine branch is a chain, in increasing cost order
# E. the tree cannot promise a slot the formation has no room for - the
# invariant that made extra doctrines silently worthless
const DOCTRINE_CHAIN: Array = ["wing", "squadron", "armada", "battlegroup", "grand_fleet"]
var game_state: Node
var scene: Node
var fleet: Node
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.reset_new_game()
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
# --- A. the opening position ---------------------------------
if GameState.STARTING_SHIP_COUNT != 1:
return _fail("a new game starts with %d vessels, expected 1" % GameState.STARTING_SHIP_COUNT)
if GameState.BASE_FLEET_SIZE != 3:
return _fail("the fleet has %d slots by default, expected 3" % GameState.BASE_FLEET_SIZE)
if game_state.surviving_ship_count != GameState.STARTING_SHIP_COUNT:
return _fail("reset_new_game left %d surviving ships" % game_state.surviving_ship_count)
if game_state.max_fleet_size() != GameState.BASE_FLEET_SIZE:
return _fail("an unresearched fleet caps at %d, expected %d" \
% [game_state.max_fleet_size(), GameState.BASE_FLEET_SIZE])
print("new game: %d vessel, %d slots" % [GameState.STARTING_SHIP_COUNT, GameState.BASE_FLEET_SIZE])
# --- C + D. the doctrine chain -------------------------------
var expected: int = GameState.BASE_FLEET_SIZE
var previous_cost: int = -1
var previous_points: int = -1
var previous_id: String = "coordination"
for id in DOCTRINE_CHAIN:
var node: Dictionary = game_state.skill_node(id)
if node.is_empty():
return _fail("no skill node '%s' - the doctrine chain is broken" % id)
var bonus: int = int(node.get("fleet_bonus", 0))
if bonus <= 0:
return _fail("'%s' is in the doctrine chain but grants no fleet_bonus" % id)
if String(node.get("requires", "")) != previous_id:
return _fail("'%s' requires '%s', expected it to follow '%s' - the branch is not one chain" \
% [id, node.get("requires", ""), previous_id])
# Later tiers must not be cheaper than earlier ones, or the
# order the player meets them stops matching what they cost.
var cost: int = int(node.get("cost", 0))
var points: int = int(node.get("research_points", 0))
if cost <= previous_cost:
return _fail("'%s' costs %d ore, not more than the tier before it (%d)" % [id, cost, previous_cost])
if points < previous_points:
return _fail("'%s' costs %d research points, fewer than the tier before it (%d)" \
% [id, points, previous_points])
previous_cost = cost
previous_points = points
previous_id = id
game_state.unlocked.append(id)
expected += bonus
if game_state.max_fleet_size() != expected:
return _fail("after researching '%s' the fleet caps at %d, expected %d" \
% [id, game_state.max_fleet_size(), expected])
print(" %-12s +%d -> %2d slots (%d ore, %d research)" % [id, bonus, expected, cost, points])
# --- E. no doctrine may be swallowed by the formation ---------
# max_fleet_size() clamps to the formation's length, so a doctrine
# added without extending DEFAULT_FORMATION would cost the player
# real resources and grant nothing.
if expected > GameState.DEFAULT_FORMATION.size():
return _fail("the doctrines promise %d slots but DEFAULT_FORMATION only defines %d - the last ones buy nothing" \
% [expected, GameState.DEFAULT_FORMATION.size()])
print("full doctrine chain reaches %d slots, with %d formation positions defined" \
% [expected, GameState.DEFAULT_FORMATION.size()])
# --- B. what actually flies ----------------------------------
game_state.reset_new_game()
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
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)
if fleet.ships.size() != GameState.STARTING_SHIP_COUNT:
return _fail("a fresh scene spawned %d vessels, expected %d" \
% [fleet.ships.size(), GameState.STARTING_SHIP_COUNT])
print("a fresh game really flies %d vessel" % fleet.ships.size())
# The two empty slots are what the opening is for, so they have to
# be fillable without researching anything.
for i in range(GameState.BASE_FLEET_SIZE - GameState.STARTING_SHIP_COUNT):
if not fleet.add_ship(GameState.VESSEL_INTERCEPTOR):
return _fail("could not crew slot %d of the starting fleet" % (i + 2))
if fleet.ships.size() != GameState.BASE_FLEET_SIZE:
return _fail("the fleet holds %d after filling its slots, expected %d" \
% [fleet.ships.size(), GameState.BASE_FLEET_SIZE])
# And the cap has to actually bite past that.
if fleet.add_ship(GameState.VESSEL_INTERCEPTOR):
return _fail("a %dth vessel joined an unresearched fleet of %d slots" \
% [fleet.ships.size(), GameState.BASE_FLEET_SIZE])
print("the starting slots fill to %d and refuse a %dth" \
% [GameState.BASE_FLEET_SIZE, GameState.BASE_FLEET_SIZE + 1])
print("TEST_FLEET_GROWTH: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_FLEET_GROWTH: FAIL (%s)" % message)
quit(1)
return true

View file

@ -20,21 +20,15 @@ func _initialize() -> void:
func _process(delta: float) -> bool:
elapsed += delta
if stage == 0:
# A guaranteed gas source near the spawn is now a DEV MODE guarantee
# rather than something every game gets - a normal game has to go and
# find one. Dev mode is therefore where this premise still holds, and
# where a play-tester reaching gas quickly still matters.
game_state.dev_mode = true
var nearby_chunk: Vector2i = Vector2i.ZERO
for chunk in WorldGen.DEV_PLANET_CHUNKS:
if String(WorldGen.DEV_PLANET_CHUNKS[chunk]) == "gas_giant":
nearby_chunk = chunk
var nearby_chunk: Vector2i = WorldGen.NEARBY_GAS_GIANT_CHUNK
var nearby_data: Dictionary = WorldGen.planet_data_at_chunk(nearby_chunk)
var nearby_position: Vector2 = WorldGen.chunk_origin(nearby_chunk) + nearby_data.local_pos
if not WorldGen.has_planet_at_chunk(nearby_chunk):
return _fail("dev mode's gas giant chunk does not contain a planet")
return _fail("nearby gas giant chunk does not contain a planet")
if WorldGen.biome_at_chunk(nearby_chunk) != WorldGen.Biome.SPACE:
return _fail("nearby gas giant is not in the normal space biome")
if String(nearby_data.get("definition_id", "")) != "gas_giant":
return _fail("dev mode's guaranteed planet is not a gas source")
return _fail("nearby guaranteed planet is not a gas source")
if nearby_position.length() > WorldGen.CHUNK_SIZE * 2.0:
return _fail("nearby gas giant spawned too far away: %s" % nearby_position)

View file

@ -1,130 +0,0 @@
extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_hud_layout.gd
# The HUD's top-anchored button columns must not collide with the resource
# readout, and must not collide with each other.
#
# Both halves are regressions that actually shipped:
# - ResourceReadout builds one row per resource type AT RUNTIME, so the
# scene's authored offset_bottom understated it and the build button,
# at a hardcoded y, ended up underneath the resource list.
# - Seating the columns below the readout then exposed a second hardcoded
# y: _layout_context_buttons() wrote the action row back to 324 on the
# next context change, dropping it on top of the moved build button.
#
# Both were a literal coordinate that encoded "below the thing above me" and
# stopped being true when the thing above moved. So this asserts the
# RELATIONSHIP rather than any particular coordinate - a fresh hardcoded y
# fails here whatever value someone picks.
#
# Covers:
# A. every HUD button clears the resource readout's real bottom
# B. no two simultaneously-usable buttons overlap each other
# C. both still hold after _layout_context_buttons() runs, collapsed and
# expanded - the path that broke
# D. it holds with MORE resource types than the scene was authored for,
# which is the direction this drifts
var scene: Node
var controller: Node
var readout: Node
var stage: int = 0
var elapsed: float = 0.0
# Deliberately share a row: a building either offers its own action or a
# hangar, never both, so their rects coinciding is the design.
const EXCLUSIVE_PAIRS: Array = [["building_action_button", "hangar_button"]]
func _initialize() -> void:
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
controller = scene.get_node("BuildController")
readout = scene.get_node("UI/ResourceReadout")
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.2:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
if not _check("as laid out at load"):
return true
# --- C. the path that broke it ------------------------------
controller.context_actions_expanded = false
controller._layout_context_buttons()
if not _check("after _layout_context_buttons() collapsed"):
return true
controller.context_actions_expanded = true
controller._layout_context_buttons()
if not _check("after _layout_context_buttons() expanded"):
return true
print("TEST_HUD_LAYOUT: PASS")
quit(0)
return true
return false
func _buttons() -> Dictionary:
return {
"build_button": controller.build_button,
"transfer_button": controller.transfer_button,
"action_button": controller.action_button,
"landing_button": controller.landing_button,
"building_action_button": controller.building_action_button,
"hangar_button": controller.hangar_button,
}
func _check(context: String) -> bool:
var buttons: Dictionary = _buttons()
var names: Array = buttons.keys()
# --- A. clear of the resource readout ---------------------------------
# content_bottom() is the readout's REAL extent, from the live resource
# list - not the stale height the scene happens to declare.
var readout_bottom: float = readout.content_bottom()
for name in names:
var button: Control = buttons[name]
if button == null:
continue
if button.position.y < readout_bottom:
return _fail("%s sits at y %.0f, inside the resource readout which reaches %.0f (%s)" \
% [name, button.position.y, readout_bottom, context])
# --- B. clear of each other -------------------------------------------
for i in range(names.size()):
for j in range(i + 1, names.size()):
var a_name: String = names[i]
var b_name: String = names[j]
if _is_exclusive(a_name, b_name):
continue
var a: Control = buttons[a_name]
var b: Control = buttons[b_name]
if a == null or b == null:
continue
if a.get_rect().intersects(b.get_rect()):
return _fail("%s %s overlaps %s %s (%s)" \
% [a_name, a.get_rect(), b_name, b.get_rect(), context])
print("clear of the readout (reaches %.0f) and of each other - %s" % [readout_bottom, context])
return true
func _is_exclusive(a: String, b: String) -> bool:
for pair in EXCLUSIVE_PAIRS:
if (a == pair[0] and b == pair[1]) or (a == pair[1] and b == pair[0]):
return true
return false
func _fail(message: String) -> bool:
print("TEST_HUD_LAYOUT: FAIL (%s)" % message)
quit(1)
return false

View file

@ -1 +0,0 @@
uid://65cagevj7qbj

View file

@ -35,14 +35,6 @@ func _process(delta: float) -> bool:
return false
if not menu.editor_button.enabled:
return _fail("map editor button is not enabled (#48 shipped)")
# Both development entries follow the same rule, and the rule is
# what is under test rather than the concrete boolean - this run
# is from Godot, so it exercises the "shown" branch.
var developing: bool = menu.is_developer_build()
if menu.editor_button.visible != developing or menu.editor_label.visible != developing:
return _fail("map editor entry visibility did not follow is_developer_build()")
if menu.dev_button.visible != developing or menu.dev_label.visible != developing:
return _fail("dev mode entry visibility did not follow is_developer_build()")
if menu.quit_button.visible != (not OS.has_feature("web")):
return _fail("quit button visibility did not follow OS.has_feature('web')")
@ -82,10 +74,8 @@ func _process(delta: float) -> bool:
if elapsed < 0.3:
return false
var on_main: bool = current_scene != null and current_scene.name == "Main"
# A normal game now starts with NOTHING banked - the opening
# mining run is the tutorial, and 500 free ore skipped it.
var reset_ok: bool = game_state.rocks == 0 and game_state.buildings.is_empty() \
and game_state.surviving_ship_count == GameState.STARTING_SHIP_COUNT \
var reset_ok: bool = game_state.rocks == 500 and game_state.buildings.is_empty() \
and game_state.surviving_ship_count == GameState.BASE_FLEET_SIZE \
and game_state.planet_capture.is_empty()
print("on_main=%s reset_ok=%s rocks=%d buildings=%d ship_count=%d" % [
on_main, reset_ok, game_state.rocks, game_state.buildings.size(),

View file

@ -19,7 +19,6 @@ var cw: IconButton
var stage: int = 0
var elapsed: float = 0.0
var angle_at_hold_start: float = 0.0
var hold_amount_at_start: float = 0.0
var angle_after_hold: float = 0.0
# GameState (an autoload) is not available as a bare identifier from a
# script that extends SceneTree - fetch it via the tree root instead.
@ -46,13 +45,6 @@ func _process(delta: float) -> bool:
0:
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
# Crew the fleet HERE, not in _initialize(): work done there can
# be undone by the engine's deferred tree-entry finalization, the
# same reason the set_process(false) calls above sit at this
# point. A new game flies one vessel now, and the checks below
# index ships[1].
game_state.unlocked.append_array(["wing", "squadron", "armada"])
fleet.spawn_ships(5, Vector2.ZERO)
# Hold the button down without releasing it.
angle_at_hold_start = _angle()
cw.pressed_down = true
@ -135,7 +127,6 @@ func _process(delta: float) -> bool:
return true
angle_at_hold_start = _angle()
hold_amount_at_start = controller.pivot_hold_amount
elapsed = 0.0
stage = 2
return false
@ -144,12 +135,7 @@ func _process(delta: float) -> bool:
return false
# Both fingers still down: the formation must have kept turning
# while the stick kept re-issuing move orders every frame.
# The ACCUMULATED turn, not the angle difference. Comparing
# formation angles wraps at +/-pi, so a hold that happened to
# rotate a full turn read as no rotation at all - and how far it
# gets in 0.4s depends on frame times, so this failed only under
# load. pivot_hold_amount is the same quantity without the wrap.
var turned_while_steering: float = controller.pivot_hold_amount - hold_amount_at_start
var turned_while_steering: float = absf(angle_difference(angle_at_hold_start, _angle()))
print("turned %.3f rad while steering" % turned_while_steering)
if turned_while_steering < 0.05:
print("TEST_PIVOT_CONTROLS: FAIL (no rotation while the stick was held)")

View file

@ -64,12 +64,6 @@ func _initialize() -> void:
current_scene = main_scene
fleet = main_scene.get_node("Fleet")
# A new game now flies STARTING_SHIP_COUNT (1) vessel into BASE_FLEET_SIZE
# (3) slots. This test needs a crewed fleet, so it asks for one outright
# rather than inheriting whatever a fresh game happens to open with -
# which is exactly what changed underneath it.
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
fleet.spawn_ships(5, Vector2.ZERO)
planet_field = main_scene.get_node("PlanetField")
landing_controller = main_scene.get_node("LandingController")
asteroid_field_ref = main_scene.get_node("AsteroidField")

View file

@ -20,13 +20,7 @@ func _initialize() -> void:
for x in range(-15, 15):
for y in range(-15, 15):
var chunk: Vector2i = Vector2i(x, y)
# Not a black hole: it destroys ships on contact, and this
# scan takes the FIRST planet it finds. That used to be the gas
# giant guaranteed in the origin chunk; with that guarantee gone
# the scan can land the fleet on something that kills it, which
# is not what either of these tests is about.
if WorldGen.has_planet_at_chunk(chunk) \
and String(WorldGen.planet_data_at_chunk(chunk).get("definition_id", "")) != "black_hole":
if WorldGen.has_planet_at_chunk(chunk):
target_chunk = chunk
found = true
break
@ -63,18 +57,6 @@ func _process(delta: float) -> bool:
# complete (fleet.ships is reliably populated by now), it's safe.
main_scene.get_node("AsteroidField").set_process(false)
main_scene.get_node("EnemySpawner").set_process(false)
# Crew the fleet HERE rather than in _initialize(): work done
# there can be undone by the engine's deferred tree-entry
# finalization, the same reason the set_process(false) calls
# above live at this point. A new game flies one vessel now.
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
fleet.spawn_ships(5, Vector2.ZERO)
# Silencing the field does not remove what it already placed, and
# those keep drifting - a rock taking the fleet out mid-test reads
# here as a landing failure.
for rock in get_nodes_in_group("asteroids"):
if is_instance_valid(rock):
rock.queue_free()
# PlanetField only scans chunks near the fleet's CURRENT
# position, so it never even looks at target_chunk unless the

View file

@ -61,10 +61,6 @@ func _initialize() -> void:
# --- F. a fresh game still reaches a crafter and an extractor -------------
game_state.reset_new_game()
# can_unlock() also checks the ore price, and a new game now banks
# nothing - the opening mining run is the point. Stock the ore so this
# checks what it says it checks: reachability at ZERO RESEARCH POINTS.
game_state.rocks = 500
if not game_state.can_unlock("shipyards"):
return _fail("shipyards is not reachable with 0 research points")
game_state.unlock("shipyards")

View file

@ -55,12 +55,6 @@ func _initialize() -> void:
scene = load("res://Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
# A new game now flies STARTING_SHIP_COUNT (1) vessel into BASE_FLEET_SIZE
# (3) slots. This test needs a crewed fleet, so it asks for one outright
# rather than inheriting whatever a fresh game happens to open with -
# which is exactly what changed underneath it.
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
fleet.spawn_ships(5, Vector2.ZERO)
controller = scene.get_node("BuildController")

View file

@ -104,10 +104,6 @@ func _process(delta: float) -> bool:
if elapsed < 0.3:
return false
# --- crafter adds a ship --------------------------------------
# Crafting adds a FOURTH vessel below, one past the 3 slots a
# new game has, so make room first - the subject here is the
# crafter, not the fleet cap.
game_state.unlocked.append_array(["wing", "squadron", "armada"])
fleet.spawn_ships(3, Vector2.ZERO)
var before_ships: int = fleet.ships.size()
var before_rocks: int = game_state.rocks

View file

@ -25,13 +25,7 @@ func _initialize() -> void:
for x in range(-15, 15):
for y in range(-15, 15):
var chunk: Vector2i = Vector2i(x, y)
# Not a black hole: it destroys ships on contact, and this
# scan takes the FIRST planet it finds. That used to be the gas
# giant guaranteed in the origin chunk; with that guarantee gone
# the scan can land the fleet on something that kills it, which
# is not what either of these tests is about.
if WorldGen.has_planet_at_chunk(chunk) \
and String(WorldGen.planet_data_at_chunk(chunk).get("definition_id", "")) != "black_hole":
if WorldGen.has_planet_at_chunk(chunk):
target_chunk = chunk
found = true
break
@ -59,18 +53,6 @@ func _process(delta: float) -> bool:
0:
main_scene.get_node("AsteroidField").set_process(false)
main_scene.get_node("EnemySpawner").set_process(false)
# Crew the fleet HERE rather than in _initialize(): work done
# there can be undone by the engine's deferred tree-entry
# finalization, the same reason the set_process(false) calls
# above live at this point. A new game flies one vessel now.
root.get_node("GameState").unlocked.append_array(["wing", "squadron", "armada"])
fleet.spawn_ships(5, Vector2.ZERO)
# Silencing the field does not remove what it already placed, and
# those keep drifting - a rock taking the fleet out mid-test reads
# here as a landing failure.
for rock in get_nodes_in_group("asteroids"):
if is_instance_valid(rock):
rock.queue_free()
var vicinity_pos: Vector2 = WorldGen.chunk_origin(target_chunk) + Vector2.ONE * WorldGen.CHUNK_SIZE * 0.5
for ship in fleet.ships:
ship.position = vicinity_pos