Dev Log โ 21 April 2026
I had multiple outstanding bugs from the previous session plus some new issues I’d spotted. Got through all of them:
- Power-ups not spawning; the export property was never assigned in the editor
- Level not completing, caused by
WAVE_CLEAR_DELAY = 0.0caused the timer check to never fire - Asteroids drifting off-screen leaving the player with nothing to do
- Endless mode would show the Level Complete screen instead of looping
- Player rank badge showing the wrong rank on start
- Settings button hover box on the Main Menu was far wider than its icon
Root causepowerup_scene was declared as @export var, which means it must be manually assigned in the Godot Editor inspector. If the editor step was skipped, the variable stays null and _try_spawn_powerup exits immediately on its null-check.
Fix
Removed the @export and replaced with a direct preload() call. The scene is always the same file so there’s no reason to make it configurable via the editor:
This change also removes the risk of a future scene reload forgetting to re-assign the export.
Root causeWAVE_CLEAR_DELAY = 0.0 (set to keep the screen always busy). In _process(), the completion check was if wave_clear_timer > 0.0. Setting the timer to exactly 0.0 makes this check permanently false, so _next_wave_or_complete() is never called.
Fix
Both places that set the timer now branch on the zero-delay case and call the function directly via call_deferred() instead of going through the timer:
The deferred call ensures we don’t accidentally call into wave logic mid-physics-frame. This fix was applied to both _on_asteroid_destroyed and the off-screen cleanup function.
Previously asteroids drifted freely and were deleted by a periodic off-screen cleanup job after travelling 500px past the screen edge. This left gaps where no asteroids were visible. I also wanted all new wave spawns to come from outside the playable area and drift inward.
Invisible Bounce Wall
Asteroid.gd now overrides _integrate_forces, the correct hook for modifying a RigidBody2D’s state without fighting the physics engine. An invisible boundary sits 100px outside each screen edge. When an asteroid crosses it, its velocity component on that axis is reflected (sign-flipped) and its position is clamped back to the wall:
Because the bounce wall is outside the visible screen (100px past the edge), asteroids briefly become invisible before they return, giving the effect of a hard invisible barrier just outside view.
Cleanup Safety Net Updated
The periodic off-screen cleanup in GameScene was kept as an extreme safety valve (raised from 500px to 1200px) in case something glitches through the boundary. The cleanup interval was also relaxed from 3s to 5s since it should almost never trigger.
Spawn Rules: Already Met
New wave spawns already follow the required rules: ~25% spawn at on-screen quadrant positions at the moment the wave begins (the “level load burst”), while the remaining 75% enter from the 8 fixed edge points and drift toward the center. Split asteroids from destroyed parents spawn at the parent’s last position (on-screen), which is expected behaviour. No changes were needed to the spawn system itself.
Root cause_next_wave_or_complete() unconditionally called _level_complete() after all 3 waves. In endless mode (gm.current_level == 0) this loaded the Level Complete screen and incremented the level, which is completely wrong behaviour.
Fix
Added an endless-mode branch that resets current_wave to 0 and restarts the wave cycle, looping forever:
Wave difficulty still scales with _gm.current_level via _get_wave_composition() and _spawn_interval(). In endless mode current_level = 0, so the game uses the easiest settings permanently. That is intentional for a relaxed endless experience. If escalating difficulty is wanted in endless, that can be driven off a separate session counter later.
Root causeGameManager.load_game() restored current_rank from the save file verbatim without verifying it against total_score. If a save file was written by an older build before rank-up logic existed, or if the save was otherwise inconsistent, current_rank could be 0 even when total_score had exceeded multiple rank thresholds. The HUD then displays the rank-0 badge regardless of actual progress.
Rank Recalculation on Load
After all save values are loaded, _recalculate_rank() walks the threshold array and sets current_rank to the highest index that total_score satisfies. This always results in a rank consistent with the actual score:
Called at the end of load_game(). Nukes are not retroactively re-awarded since nukes_held is already loaded from the save.
The Settings button (bottom-right) had a hit area of 344ร248px while the Ships button (bottom-left, which I’d confirmed was correct) is 202ร256px. The oversized Settings button made the hover glow extend far beyond the icon.
Fix
Resized BtnSettings to 202ร256px, flush to the right screen edge, matching the proportions of BtnShop:
| Property | Before | After |
|---|---|---|
| offset_left | 1632 | 1718 |
| offset_right | 1976 | 1920 |
| offset_bottom | 1071 | 1079 |
Width is now 202px (matching BtnShop). The button is right-edge-flush at x=1920, the same way BtnShop is left-edge-flush at x=3.
Shield overlay was set to Vector2(1.6, 1.6) which made it far larger than the ship. Reduced to Vector2(0.9, 0.9). The shield textures are already larger than the ship sprite, so 0.9ร sits just outside the ship boundary without looking oversized. Fine-tune in 0.1 increments if needed.
Three separate issues were causing asteroids to be invisible for extended periods, leaving the player with nothing to shoot:
1. Bounce Wall Too Far Out (Asteroid.gd)
IssueBOUNCE_MARGIN was 100px outside the screen edge. An asteroid could travel 100px off-screen, completely invisible, before bouncing back. Near the end of a wave when only a few asteroids remained, the player could be left staring at an empty screen for several seconds.
Fix Reduced to 20px. Asteroids barely clip the edge before returning, staying visible at all times.
2. Spawn Points Too Far From Screen (GameScene.gd)
IssueSPAWN_MARGIN was 200px outside the screen. A slow large asteroid (50 px/s minimum speed) took over 4 seconds just to enter the visible area from its spawn point, and the player saw nothing during this time.
Fix Reduced to 80px. Worst-case entry time is now under 2 seconds; fast asteroids appear almost immediately.
3. On-Screen Burst at Wave Start Removed (GameScene.gd)
Issue_start_wave() spawned ~25% of the wave’s asteroids directly inside the playable area. This broke the design rule that all new asteroids should enter from outside the screen.
Fix Removed the on-screen burst entirely. Every asteroid in every wave now queues from one of the 8 edge spawn points and drifts toward the center. The _quadrant_positions() helper was deleted as it is no longer used.
4. First Spawn Delay Removed (GameScene.gd)
Issue After the queue was built, _spawn_timer was set to _spawn_interval() (1.2s at level 1), meaning the very first asteroid waited over a second before spawning, leaving an empty screen at wave start.
Fix Timer is now set to 0.0 after queue build so the first asteroid spawns on the very next frame.
Power-up spawn behaviour: no change needed
Power-ups already spawn at the asteroid’s destroyed position and drift from there, the “drops on destroy” approach. This is correct behaviour and was left unchanged.
Between waves there was no visual feedback. The screen went quiet and the player had no warning the next wave was coming. Added a red flashing WAVE X label that appears for ~1.8 seconds before each new wave starts.
Implementation
_next_wave_or_complete() now routes through _show_wave_warning() instead of calling _start_wave() directly. The warning creates a temporary CanvasLayer overlay (layer 8, above the game), adds a centred 96px red label, flashes it 4 times via Tween, then frees the overlay and calls _start_wave() via a tween callback:
Works for both levelled and endless modes. In endless, the wave counter resets to 0 before the warning so it correctly shows “WAVE 1” at the start of each new cycle.
The previous Level Complete screen used a plain panel with text labels and no visual identity. Rebuilt to match the original Buildbox screenshots: space background, Score/Best on either side, centred content with a cyan tech arc frame, selected ship inside the frame, animated stars, rank promotion message, and styled Continue/Retry buttons.
Layout (1920ร1080)
| Element | Position | Notes |
|---|---|---|
| Background | Full screen | Same space BG as game scene |
| Score label | Left, x 40โ320, y 400โ545 | Cyan header + white value |
| Best label | Right, x 1600โ1880, y 400โ545 | Same style |
| “Level Complete” | Centre, y 108โ162 | 34px, pale blue |
| “Congratulations” | Centre, y 162โ258 | 64px, white |
| Stars ร3 | Centre, y 258โ370 | Animated, 92px |
| Arc frame | Centre, y 344โ672 | ship_select_frame.png |
| Ship display | Inside frame, y 372โ618 | Level Complete ship variant |
| Thrusters | Below ship, y 606โ672 | Thrusters.png |
| Rank message | Centre, y 678โ772 | “You Have Been Promoted / [Rank]” |
| Continue button | Centre, y 788โ876 | Cyan border, pill shape |
| Retry button | Centre, y 892โ958 | Smaller, same style |
Star Animation
Stars are hidden (scale 0, alpha 0) at build time and animated in sequence via create_tween(). Each star is staggered by 0.42 s:
- Earned star: fades in, overshoots to scale 1.45 (TRANS_BACK EASE_OUT), settles to 1.0, in gold (#FFB800)
- Unearned star: fades in, scales to 0.75, in dim grey
Scene Architecture
All visual nodes are explicitly defined in LevelComplete.tscn and are fully visible in the Godot editor. LevelComplete.gd uses @onready references to reach those nodes, and contains no _build_ui() and creates no nodes at runtime. Content (score, ship texture, rank text) is populated in _populate(), called from setup(stars, score, completed_level) after the scene is added to the tree.
Assets Used
- Game Assests/UI/Level Complate/ship_select_frame.png: cyan tech arc
- Game Assests/UI/Level Complate/Ships/[ship].png: flat ship art per variant
- Game Assests/UI/Level Complate/Ships/Thrusters.png: flame sprite
After the Level Complete redesign the screen appeared completely blank in-game. Two separate root causes were found and fixed.
Cause 1: GDScript parse error with const :=
Root cause In GDScript 4, const declarations must use =. Using := (the type-inferring assignment for var) on a const is a parse error. All six asset path constants had been written with :=:
The parse failure meant the script never loaded at all, leaving the CanvasLayer empty. Fixed by changing all six constants to use =.
Cause 2: Programmatic _build_ui() invisible in editor
Root cause The script built every node at runtime inside _build_ui(). The Godot editor has no way to preview runtime-created nodes, so the scene tree showed only the bare CanvasLayer root. This made it impossible to visually inspect or adjust the layout without running the game, and any future design changes required editing code rather than the editor.
Fix Rewrote LevelComplete.tscn with every node explicitly declared in the scene file. Rewrote LevelComplete.gd to use @onready references to those nodes. The script now only handles:
- Connecting button signals in
_ready() - Populating dynamic content (score, ship texture, rank text) in
_populate() - Running the star animation in
_animate_stars() - Navigation via
_next_level()/_retry()
The screen is now fully visible and editable in the Godot editor scene view.
Root cause The level icon images (Level %d.png) already have the white level number baked into the artwork. The _make_tile() function was also adding a programmatic Label on top of the icon, coloured turquoise (Color(0.0, 0.95, 1.0)), resulting in two numbers visible on every unlocked tile: the white one from the texture and a cyan one from the label.
Fix Removed the num_lbl Label block entirely. The white number from the icon asset is the correct display.
Issue The Level Complete screen showed the rank name as text but the rank badge image was never displayed. The old script had a _RANK_PATH_FMT constant that was dropped during the @onready rewrite.
Fix
Added a RankBadge TextureRect node to LevelComplete.tscn at the left-hand side of the screen (x=50โ340, y=560โ850, 290ร290 px). This occupies the empty space below the score readout without disturbing the centred ship/stars/buttons layout.
Badge files are rank_01.png โ rank_19.png in the Silver Ranks folder. Index = current_rank + 1, clamped to [1, 19].
Sub-label Updated
The SubLabel text now shows rank name and coins earned on one line, and correctly detects whether a promotion happened this session:
Ships now auto-upgrade within their series (X1โX2โX3) as the player’s rank increases. Advancing within a series is free; coins are only needed to buy the first ship in the next series.
Rank โ Variant Mapping
| Rank range | Ship variant | Thrusters | Rockets per shot |
|---|---|---|---|
| 0โ5 (Lieutenant / Officer) | X1 | Centre only | 1 |
| 6โ11 (Squadron / Wing) | X2 | Side wings | 2 |
| 12โ18 (Group Captain+) | X3 | Centre + wings | 3 |
Auto-Upgrade Logic (GameManager.gd)
get_ship_variant() computes the correct variant from the current rank. _auto_upgrade_ship() is called from _check_rank_up(). It calculates the new ship index within the same series and updates selected_ship (and unlocks it for free) if the variant increased:
Upgrade only moves forward: if the player buys and equips a higher-tier series, that series upgrades independently.
Multi-Rocket Firing (Player.gd)
_handle_fire() now reads gm.get_ship_variant() each shot. X2 fires from two wing positions; X3 fires from all three:
All bullets travel parallel (same rotation, different spawn origin) so X3 delivers a spread that still hits a focused target at long range.
Players now earn coins each time they complete a level. Coins accumulate toward purchasing the next ship series from the shop.
Formula
| Stars earned | Coins awarded |
|---|---|
| 1 star | 15 coins |
| 2 stars | 20 coins |
| 3 stars | 25 coins |
By the time the player reaches rank 12 (X3 ship, ~93 level completions), they should have accumulated well over 600 coins, enough to purchase the DKO series X1 (600 coins). This satisfies the design goal of reaching max ship variant and having enough coins to buy the next series.
Implementation
award_level_coins(stars) was added to GameManager and called in GameScene._level_complete() after stars are calculated. It stores the amount in last_coins_earned (session-only, not persisted) so the Level Complete screen can display it.
- Mobile joystick input not wired to player movement
- Audio bus muting on session start (respect saved music_enabled / sfx_enabled)
- HUD nuke count display (
update_nukes()stub still returnspass) - Nuke activation mechanic for rank-earned nukes (stored in
nukes_held, no key binding yet) - Coin shop / IAP integration
- Endless mode difficulty scaling (currently stays at level-0 speed forever)
- Level Complete screen: visual polish pass still needed