2026-08-15 14:43:46 +02:00
|
|
|
extends Node2D
|
|
|
|
|
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
# The menu stacks whatever entries are actually available, rather than
|
|
|
|
|
# trusting the positions authored in the scene.
|
|
|
|
|
#
|
Stacking distorders, menu layouts, lab rewards, enemy detail, artillery cross
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>
2026-08-18 16:54:46 +02:00
|
|
|
# Two of the three entries only exist when the game is run from Godot, so a
|
|
|
|
|
# shipped build shows one entry and an editor run shows three. There is no
|
|
|
|
|
# Quit: a web build has nothing to quit TO, and on Android the system back
|
|
|
|
|
# gesture already does it. Fixed
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
# 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
|
|
|
|
|
|
2026-08-15 14:43:46 +02:00
|
|
|
@onready var play_button: IconButton = $UI/PlayButton
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
@onready var play_label: Label = $UI/PlayLabel
|
2026-08-15 14:43:46 +02:00
|
|
|
@onready var editor_button: IconButton = $UI/EditorButton
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
@onready var editor_label: Label = $UI/EditorLabel
|
|
|
|
|
@onready var dev_button: IconButton = $UI/DevModeButton
|
|
|
|
|
@onready var dev_label: Label = $UI/DevModeLabel
|
2026-08-15 14:43:46 +02:00
|
|
|
@onready var status_label: Label = $UI/StatusLabel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
play_button.pressed.connect(_on_play_pressed)
|
|
|
|
|
editor_button.pressed.connect(_on_editor_pressed)
|
Add the planet setup map editor (#48)
PlanetSetupEditor authors PlanetSetup resources: pick a surface
profile (drives the real lunar_surface background, not a blank
sheet), place/move/delete capture points and enemies from a palette
generated straight off EnemyDefinitions.all() (a new enemy definition
shows up with no editor change), pan/pinch-zoom the canvas with the
same touch idiom as GameController, then save to
res://PlanetSetups/<id>.tres via ResourceSaver - reachable now from
the main menu's Map Editor button. The mutation methods
(new_setup/load_setup/add_*/move_*/remove_*/save_setup) are the whole
model and work standalone, which is what lets
tests/test_setup_editor.gd drive placement/move/delete/save/reload
directly without simulating touch input.
Every cross-file reference is a preloaded script const rather than a
bare global class name, matching #42/#44's load-order notes - this
file is instantiated from other scripts' function bodies, never as a
script-mode test entry point itself, so it wasn't at risk the same way
the test files were, but the pattern is kept consistent throughout.
Part of the #40 conquest-mechanic breakdown. This closes out the
breakdown: #41-#48 are now all implemented, tested, and merged.
2026-08-15 21:24:28 +02:00
|
|
|
editor_button.enabled = true
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
dev_button.pressed.connect(_on_dev_mode_pressed)
|
|
|
|
|
dev_button.enabled = true
|
2026-08-15 14:43:46 +02:00
|
|
|
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
# 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],
|
Stacking distorders, menu layouts, lab rewards, enemy detail, artillery cross
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>
2026-08-18 16:54:46 +02:00
|
|
|
[dev_button, dev_label]]:
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
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
|
|
|
|
|
|
2026-08-15 14:43:46 +02:00
|
|
|
|
|
|
|
|
func _on_play_pressed() -> void:
|
|
|
|
|
GameState.reset_new_game()
|
#84: move 215 root files into a folder hierarchy
Everything lived in one flat directory, so a diff gave no clue what area it
touched and finding the file you wanted meant knowing its exact name. Twelve
folders now, grouped by what a thing IS rather than what type it is - a
vessel's definition, its component scenes and its upgrade resources sit
together, because that is the set you read when you change one of them.
buildings core controllers enemies logistics planets
resources shaders surface ui vessels vfx world
332 files moved and every res:// reference rewritten - 306 files touched,
including the autoload and main-scene paths in project.godot. Nothing renamed:
this is purely where files sit, so the diff is a pile of pure moves and no
behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at
the root, where Godot and the export preset expect them.
Verified three ways, because a suite alone would not have caught all of it:
the full 117 pass, the web export builds clean, and the game renders.
The export is why that matters. The FIRST export after the move failed with
"Attempt to open script 'res://GameState.gd'" - the old path - while
project.godot on disk plainly said res://core/GameState.gd. That is this
project's documented Android FUSE mtime trap: Godot served a cached copy of a
file whose content had changed. A `touch` fixed it. Nothing in the test suite
would have found that, because the suite runs from a copy on a native
filesystem - the one place the bug cannot happen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
|
|
|
_start("res://world/Main.tscn")
|
Stacking distorders, menu layouts, lab rewards, enemy detail, artillery cross
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>
2026-08-18 16:54:46 +02:00
|
|
|
|
|
|
|
|
|
Fix the web build: a wasm trap on integer % 0, and buttons that never fired
The web export reached the main menu and went no further. Two separate bugs,
one hiding the other.
BUTTONS NEVER FIRED. A browser on a phone delivers the touch DOWN and never
the release. IconButton emits `pressed` only on release, deliberately, so
that dragging off cancels - so on the web it never emitted at all, on every
button in the game. fire_on_press (default OS.has_feature("web")) completes
the press there instead, and does not latch, since nothing is coming to
unlatch it. The cost is drag-off-to-cancel, which every other platform keeps.
Measured on the device rather than guessed: an on-screen probe reported
`touch=1 mouse=1 held=y` after one tap - one down, one emulated mouse down,
the button latched through ordinary GUI routing, then nothing.
THE ENGINE THEN TRAPPED. With the button working, pressing Play loaded and
built Main.tscn and the whole engine stopped mid-frame, page still alive.
An error overlay in a custom HTML shell finally showed why:
Uncaught RuntimeError: remainder by zero
An integer % 0. Wasm TRAPS on that; native builds silently tolerate it. So
the bug was equally present on Android and in all 100 tests, doing nothing
in both - which is why nothing runnable here could ever have caught it, and
why reasoning from frozen screenshots got nowhere for several rounds. Every
variable modulo is now guarded: the hangar grid's `i % columns`, planet
naming's `% names.size()`, and two in the map editor.
WHAT IS KEPT, AND WHY
- web_shell.html mirrors console errors onto the page, hidden until one
occurs. Godot's errors go to the browser console, which is unreachable on
a phone; this found in one shot what inference had failed to find all
evening. It costs nothing when there is nothing to report.
- MainMenu._start does load -> instantiate -> add_child by hand instead of
change_scene_to_file. That call does all three opaquely, so a failure is
indistinguishable from an unpressed button; split, each step reports.
- PlanetAtmosphereController defers the forced world populate out of the
frame that builds Main.tscn. It did not fix the hang - the trap did - but
25 chunks of asteroids and 49 of planets do not belong in the one frame
that already constructs 197 nodes, and nothing can paint until it ends.
Removed the direct-input-routing experiment and its test: it was written on
the theory that touches were not reaching Controls, which `held=y` later
disproved, and its set_input_as_handled() swallowed the very events the
probe was reading.
Full suite green at 100/100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:08:09 +02:00
|
|
|
# Loaded, built and added by hand rather than through change_scene_to_file().
|
|
|
|
|
# That call does all three in one opaque step, so when the web build hung
|
|
|
|
|
# there was no way to tell which part failed. Split out, each step can say so
|
|
|
|
|
# - and a scene change that fails otherwise leaves the menu up, which looks
|
|
|
|
|
# exactly like an unpressed button.
|
Stacking distorders, menu layouts, lab rewards, enemy detail, artillery cross
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>
2026-08-18 16:54:46 +02:00
|
|
|
func _start(scene_path: String) -> void:
|
Fire buttons on press where the browser never delivers a release
Every button in the web build was dead - reported as "Play does nothing".
Diagnosed on the deployed build with a temporary on-screen input probe,
because none of it reproduces here: the suite passes, and pressing Play under
the real gl_compatibility renderer (the same path WebGL2 takes) reaches
Main.tscn fine. The probe reported, from a phone browser:
#6 touch down @ 362,781
play rect 296,725 128x128
hit=YES
Three taps, six events, every one a DOWN and not a single UP - and the button
left drawn in its bright held state, pressed_down stuck true because nothing
ever came to clear it. So touches arrive, their coordinates are correct, and
they land inside the button; the browser simply never delivers the release,
in _gui_input or in raw _input.
IconButton emits `pressed` only on release, deliberately, so that dragging
off a button cancels it. With no release that means it never emits at all -
on every button in the game, not just Play. Which also explains why the
earlier suspects went nowhere: the scene change was never reached, so it was
never the scene change, and hit=YES ruled out coordinate mapping.
fire_on_press defaults to OS.has_feature("web"). There, a press completes the
tap itself and does NOT latch, since nothing is coming to unlatch it. Every
other platform is untouched and keeps drag-off-to-cancel.
The cost, stated plainly: a web tap can no longer be cancelled by sliding off
before lifting, and that applies to destructive buttons too. The alternative -
keeping release-firing and treating a new press as completing the previous
one - preserves cancellation but leaves the very first tap dead, which is
worse.
tests/test_icon_button_press_fires.gd drives the flag directly, since a
headless Linux run cannot BE a web build: one tap emits exactly once, a late
release does not emit again, the button does not latch, and with the flag off
the release still drives the press and dragging off still cancels.
MainMenu keeps the load/start bisection introduced while chasing this - a
failed change_scene_to_packed otherwise leaves the menu up, which looks
exactly like an unpressed button.
Full suite green at 100/100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:02:20 +02:00
|
|
|
var packed: PackedScene = load(scene_path)
|
|
|
|
|
if packed == null:
|
|
|
|
|
status_label.text = "Could not load %s" % scene_path.get_file()
|
|
|
|
|
return
|
Fix the web build: a wasm trap on integer % 0, and buttons that never fired
The web export reached the main menu and went no further. Two separate bugs,
one hiding the other.
BUTTONS NEVER FIRED. A browser on a phone delivers the touch DOWN and never
the release. IconButton emits `pressed` only on release, deliberately, so
that dragging off cancels - so on the web it never emitted at all, on every
button in the game. fire_on_press (default OS.has_feature("web")) completes
the press there instead, and does not latch, since nothing is coming to
unlatch it. The cost is drag-off-to-cancel, which every other platform keeps.
Measured on the device rather than guessed: an on-screen probe reported
`touch=1 mouse=1 held=y` after one tap - one down, one emulated mouse down,
the button latched through ordinary GUI routing, then nothing.
THE ENGINE THEN TRAPPED. With the button working, pressing Play loaded and
built Main.tscn and the whole engine stopped mid-frame, page still alive.
An error overlay in a custom HTML shell finally showed why:
Uncaught RuntimeError: remainder by zero
An integer % 0. Wasm TRAPS on that; native builds silently tolerate it. So
the bug was equally present on Android and in all 100 tests, doing nothing
in both - which is why nothing runnable here could ever have caught it, and
why reasoning from frozen screenshots got nowhere for several rounds. Every
variable modulo is now guarded: the hangar grid's `i % columns`, planet
naming's `% names.size()`, and two in the map editor.
WHAT IS KEPT, AND WHY
- web_shell.html mirrors console errors onto the page, hidden until one
occurs. Godot's errors go to the browser console, which is unreachable on
a phone; this found in one shot what inference had failed to find all
evening. It costs nothing when there is nothing to report.
- MainMenu._start does load -> instantiate -> add_child by hand instead of
change_scene_to_file. That call does all three opaquely, so a failure is
indistinguishable from an unpressed button; split, each step reports.
- PlanetAtmosphereController defers the forced world populate out of the
frame that builds Main.tscn. It did not fix the hang - the trap did - but
25 chunks of asteroids and 49 of planets do not belong in the one frame
that already constructs 197 nodes, and nothing can paint until it ends.
Removed the direct-input-routing experiment and its test: it was written on
the theory that touches were not reaching Controls, which `held=y` later
disproved, and its set_input_as_handled() swallowed the very events the
probe was reading.
Full suite green at 100/100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 01:08:09 +02:00
|
|
|
var scene: Node = packed.instantiate()
|
|
|
|
|
if scene == null:
|
|
|
|
|
status_label.text = "Could not build %s" % scene_path.get_file()
|
|
|
|
|
return
|
|
|
|
|
get_tree().root.add_child(scene)
|
|
|
|
|
get_tree().current_scene = scene
|
|
|
|
|
queue_free()
|
2026-08-15 14:43:46 +02:00
|
|
|
|
|
|
|
|
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
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()
|
#84: move 215 root files into a folder hierarchy
Everything lived in one flat directory, so a diff gave no clue what area it
touched and finding the file you wanted meant knowing its exact name. Twelve
folders now, grouped by what a thing IS rather than what type it is - a
vessel's definition, its component scenes and its upgrade resources sit
together, because that is the set you read when you change one of them.
buildings core controllers enemies logistics planets
resources shaders surface ui vessels vfx world
332 files moved and every res:// reference rewritten - 306 files touched,
including the autoload and main-scene paths in project.godot. Nothing renamed:
this is purely where files sit, so the diff is a pile of pure moves and no
behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at
the root, where Godot and the export preset expect them.
Verified three ways, because a suite alone would not have caught all of it:
the full 117 pass, the web export builds clean, and the game renders.
The export is why that matters. The FIRST export after the move failed with
"Attempt to open script 'res://GameState.gd'" - the old path - while
project.godot on disk plainly said res://core/GameState.gd. That is this
project's documented Android FUSE mtime trap: Godot served a cached copy of a
file whose content had changed. A `touch` fixed it. Nothing in the test suite
would have found that, because the suite runs from a copy on a native
filesystem - the one place the bug cannot happen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
|
|
|
_start("res://world/Main.tscn")
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
|
|
|
|
|
|
2026-08-15 14:43:46 +02:00
|
|
|
func _on_editor_pressed() -> void:
|
Rework how a game starts: dev mode, a bare opening, and fleet growth
Adds a Dev Mode entry to the main menu and takes the play-testing
conveniences out of a normal game, which turn out to be the same change
from two directions.
DEV MODE. Banks 2000 of every resource, grants 500 research points, and
plants one of each planet type in the chunks around the spawn. 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 definition of "fresh game" to keep in
sync. Offered only when running from Godot - OS.has_feature("editor"), the
tag an export clears - as is the map editor. Hidden AND guarded: pressing
either anyway is refused, because visibility is presentation and the rule is
the rule.
The menu now lays its stack out in code. Hiding two of four entries with
scene-authored offsets would leave holes in a shipped build - the same
failure the HUD columns had - so it measures the entries that actually exist
and centres on them.
A NORMAL GAME NOW HANDS OVER NOTHING. No 500 banked ore (removed from both
reset_new_game and the autoload's own default, so the two cannot disagree),
and no gas giant guaranteed in the origin chunk. Both were play-testing
conveniences that had become part of the real world's shape: every player
got the same landmark in the same place. Play-testing wanted all four planet
types anyway, and a normal game wanted none of them - different needs, now
different mechanisms.
WorldGen cannot read GameState back (an autoload is not resolvable as a
global identifier that early, the hazard PlanetSetups.gd already works
around), so GameState.dev_mode's setter PUSHES the flag down. World
generation stays static and dependency-free, GameState stays the single
source of truth, and dev planting is applied after every rng draw so a dev
world and a normal one differ only in those four chunks.
FLEET SIZE. BASE_FLEET_SIZE was doing two jobs - how many slots exist AND
how many ships you start with - which forced a new game to open with a full
fleet, leaving the first ship crafter nothing to do. Split into
BASE_FLEET_SIZE (3 slots) and STARTING_SHIP_COUNT (1 vessel), so the opening
has two empty slots to fill. A third copy of the old value turned up in
PlanetAtmosphereController, hardcoded as spawn_ships(5) and silently
degraded into "however many slots exist" once max_fleet_size() clamped it.
The doctrine branch now runs 3 -> 10 slots. Wing (30 -> 15) and Squadron
(70 -> 35, 2 -> 1 points) are cut hardest: they are no longer a mid-game
luxury but the ordinary way out of the opening, and pricing them as a luxury
made the early game feel stuck. Armada follows at 90/2. Two new tiers,
Battlegroup and Grand Fleet, grant +2 each - seven single-vessel nodes in
one lane would be a long monotonous chain for a branch that only ever does
one thing. DEFAULT_FORMATION grows to 10 slots, because max_fleet_size()
clamps to its length and a doctrine past that would charge real resources
for nothing; test_fleet_growth.gd asserts that invariant directly.
TEST FALLOUT, all of it from the deliberate change rather than a defect:
- Tests that silently inherited the old 5-ship default now ask for the fleet
they need. Several had to crew it from their first frame rather than
_initialize(), where the engine's deferred tree-entry finalization undoes
it - the same hazard those tests already documented for set_process(false).
- test_planet_landing_proximity and test_return_position_matches_planet take
the FIRST chunk holding a planet and land there. The guaranteed gas giant
had been masking that this can now be a black hole, which destroys ships on
contact. They exclude it, and clear the asteroids the field already placed.
- test_pivot_controls measured rotation with angle_difference, which wraps at
+/-pi, so a hold that turned a full circle read as no rotation. Latent
flakiness that only ever showed under load; it now reads the controller's
unwrapped pivot_hold_amount.
- test_research_points was failing on ore, not points: can_unlock() checks
the ore price too, and a fresh game banks nothing now.
- test_gas_system's "a reachable gas source near the spawn" premise moved to
dev mode, which is where that guarantee now lives.
Full suite green at 97/97.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:10:14 +02:00
|
|
|
if not is_developer_build():
|
|
|
|
|
status_label.text = "Map editor only runs from Godot"
|
|
|
|
|
return
|
2026-08-15 14:43:46 +02:00
|
|
|
if editor_button.enabled:
|
#84: move 215 root files into a folder hierarchy
Everything lived in one flat directory, so a diff gave no clue what area it
touched and finding the file you wanted meant knowing its exact name. Twelve
folders now, grouped by what a thing IS rather than what type it is - a
vessel's definition, its component scenes and its upgrade resources sit
together, because that is the set you read when you change one of them.
buildings core controllers enemies logistics planets
resources shaders surface ui vessels vfx world
332 files moved and every res:// reference rewritten - 306 files touched,
including the autoload and main-scene paths in project.godot. Nothing renamed:
this is purely where files sit, so the diff is a pile of pure moves and no
behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at
the root, where Godot and the export preset expect them.
Verified three ways, because a suite alone would not have caught all of it:
the full 117 pass, the web export builds clean, and the game renders.
The export is why that matters. The FIRST export after the move failed with
"Attempt to open script 'res://GameState.gd'" - the old path - while
project.godot on disk plainly said res://core/GameState.gd. That is this
project's documented Android FUSE mtime trap: Godot served a cached copy of a
file whose content had changed. A `touch` fixed it. Nothing in the test suite
would have found that, because the suite runs from a copy on a native
filesystem - the one place the bug cannot happen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
|
|
|
_start("res://surface/PlanetSetupEditor.tscn")
|
2026-08-15 14:43:46 +02:00
|
|
|
return
|
|
|
|
|
status_label.text = "Map editor coming soon"
|
|
|
|
|
|
|
|
|
|
|