Dev Log โ 14 June 2026
Session Overview
A dedicated audio session. All remaining “ready to wire” sound effects were wired in, a full in-game music system with 18 shuffled tracks was built, the Settings menu was overhauled with three independent volume sliders replacing the fullscreen toggle, and several bugs introduced during audio wiring were fixed.
- Wired: All remaining SFX: mothership shot, asteroid hit, crystal pickup, power-up pickup, nuke explosion, ship purchase chime
- New: Persistent menu music starting at the splash screen and playing through all menus
- New: In-game background music: 18 tracks shuffled and played sequentially, stopping when gameplay starts and resuming on return to menus
- New: Enemy unit and mothership ambient engine hum (looping, low volume)
- New: Player death, shield activation, ship level-up, and rank promotion sounds
- New: Settings menu volume system: Master Volume, Music Volume, SFX Volume sliders with individual bus control
- Fixed: Three bugs:
Array[int]type error,velocityvariable shadowing, nuke physics flush error
Sound Effects: Full Wiring Pass
All “Assets on Disk: Ready to Wire” sounds from the Session 10 audit were wired via the central GameManager.play_sfx(stream) helper. This dispatches a self-freeing AudioStreamPlayer on the GameManager node so sounds survive entity destruction.
| Sound File | Trigger | Script |
|---|---|---|
| Enemy Shot Fired.wav | Mothership fires a bullet | EnemyMothership.gd ยท _shoot_at() |
| Asteroid Hit.mp3 | Asteroid takes any hit (including final) | Asteroid.gd ยท hit() |
| Crystal Pickup.mp3 | Player collects a crystal | Crystal.gd ยท _on_body_entered() |
| Power Up Pick UP.mp3 | Any power-up collected | PowerUp.gd ยท _apply() |
| Nuke Exp.mp3 | Nuke detonates | NukeExplosion.gd ยท _ready() |
| HoloTill Chime.wav | Successful ship purchase | CharacterShop.gd ยท _on_tile_pressed() |
| sci-fi-explosion.mp3 | Enemy unit or mothership destroyed | GameScene.gd ยท _on_enemy_destroyed() |
| Ship Level Up.wav | Ship crosses X1โX2 or X2โX3 threshold | GameScene.gd ยท _on_ship_leveled_up() |
| Level_up_upgrade…variation4.wav | Player rank promotion | HUD.gd ยท _on_rank_up() |
| Spaceship-large-underwater-explosion.mp3 | Player dies | GameScene.gd ยท _on_player_died() |
| spaceship Shield.mp3 | Shield power-up activated | Player.gd ยท activate_shield() |
Ambient Enemy Hum: Per-Entity Looping
Enemy units and motherships now emit a continuous engine hum for the duration of their lifetime. An AudioStreamPlayer is created as a child node in _ready() so it is automatically freed when the entity is destroyed. The finished signal is connected back to play() to loop.
| Sound | Entity | Volume |
|---|---|---|
| Alien Ship.wav | EnemyUnit | -18 dB |
| Mothership.mp3 | EnemyMothership | -15 dB |
Persistent Menu Music System
New Menu music previously started when MainMenu.tscn loaded, meaning it restarted every time the player navigated back to the main menu. It has been moved to GameManager so it starts immediately at the splash screen and continues uninterrupted through all menus.
Architecture
- A persistent
_menu_music: AudioStreamPlayerlives onGameManager(survives all scene changes) play_menu_music()plays from the start; no-op if already playing (prevents restart when navigating between menus)stop_menu_music()stops cleanly_menu_music.finishedconnected to_menu_music.playfor seamless looping
Scene Hooks
| Scene | Action |
|---|---|
| GameManager._ready() | Creates player, starts music; covers splash screen from first frame |
| GameScene._ready() | Calls stop_menu_music() then play_game_music() |
| MainMenu._ready() | Calls stop_game_music() then play_menu_music(), restarting menu music after returning from gameplay |
play_menu_music() checks _menu_music.playing before acting. Navigating main menu โ shop โ level select โ back never restarts the track mid-song. Only returning from gameplay (where it was stopped) triggers a restart. In-Game Music System (18 Tracks)
New 18 gameplay music tracks were added in a new res://Sounds/Game Music/ folder. The original files had long descriptive names; all were renamed Track 1.wav through Track 18.wav for clean resource paths.
Shuffled Playlist
The music system in GameManager shuffles all 18 track indices at the start of each gameplay session, plays them in shuffled order one at a time, then reshuffles and repeats when all tracks are exhausted. This avoids the same track playing twice in a row.
func play_game_music() -> void:
# Lazy-initialise the player on first call
_game_track_order.assign(range(GAME_MUSIC_PATHS.size()))
_game_track_order.shuffle()
_game_track_index = 0
_play_game_track()
func _on_game_track_finished() -> void:
_play_game_track() # advance to next; reshuffles when list exhausted
range() returns an untyped Array in Godot 4. Assigning it directly to Array[int] raises a runtime error. Fix: use Array[int].assign(range(n)) which accepts an untyped array and converts element-by-element. Settings Menu: Volume Slider Overhaul
NewChange The fullscreen toggle was removed. In its place, three independent volume sliders were added using existing neon UI assets from res://Game Assets/UI/Loading/.
Layout
| Row | Control | Bus Affected |
|---|---|---|
| Music | Toggle (on/off) | Music bus mute |
| Music Volume | HSlider 0โ100 | Music bus volume |
| Sound Effects | Toggle (on/off) | SFX bus mute |
| SFX Volume | HSlider 0โ100 | SFX bus volume |
| Master Volume | HSlider 0โ100 | Both buses (multiplied) |
Volume Maths
Each bus receives the product of the master and its individual level, converted to dB:
func apply_volume() -> void:
var music_db := linear_to_db(master_volume * music_volume) if master_volume * music_volume > 0.0 else -80.0
var sfx_db := linear_to_db(master_volume * sfx_volume) if master_volume * sfx_volume > 0.0 else -80.0
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Music"), music_db)
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("SFX"), sfx_db)
Setting any slider to 0 sends the bus to -80 dB (silence) rather than -inf, avoiding a Godot AudioServer edge case.
Slider Styling
Sliders are styled entirely in code via _style_slider(slider: HSlider) called for each of the three sliders in _ready():
- Track:
StyleBoxTextureusingsettings_volume.png(neon cyan outline) - Fill:
StyleBoxTextureusingsettings_volume 2.png(solid teal) - Grabber knob: Programmatic 36ร36 px cyan circle drawn via
Image.create()and converted toImageTextureat runtime, so no separate asset needed
Persistence
master_volume, music_volume, and sfx_volume are saved to user://save.cfg under the settings section. All default to 1.0 for first-run players.
Bug Fixes
Array[int] Type Assignment Error
FixGameManager.play_game_music() raised a runtime error on first call:
-- Trying to assign an array of type "Array" to a variable of type "Array[int]"
range() returns an untyped Array in Godot 4. Direct assignment to a typed Array[int] variable fails. Fixed by replacing _game_track_order = range(n) with _game_track_order.assign(range(n)).
velocity Shadowing CharacterBody2D Property
FixPlayer.gd line 1098 declared var velocity: Vector2 inside the death effect loop, shadowing CharacterBody2D.velocity. Renamed to fly_vel in both the declaration and the timer lambda closure that references it.
Nuke Physics Flush Error
FixPowerUp.gd _fire_nuke() called get_tree().current_scene.add_child(missile) directly inside _on_body_entered(), a physics callback. Godot 2D physics forbids modifying the scene tree while queries are flushing:
-- Can't change this state while flushing queries. Use call_deferred().
Fixed by changing to add_child.call_deferred(missile). The missile is still instantiated and configured immediately; it is only added to the scene tree on the next frame.
Files Changed
| File | Change | Status |
|---|---|---|
| scripts/autoload/GameManager.gd | Added play_sfx(), play/stop_menu_music(), play/stop_game_music(), 18-track shuffled playlist, master/music/sfx volume vars, apply_volume(), save/load all new settings | Done |
| scripts/game/GameScene.gd | Added stop_menu_music() + play_game_music() in _ready(); enemy explosion sound in _on_enemy_destroyed(); ship level-up sound in _on_ship_leveled_up(); player death sound in _on_player_died() | Done |
| scripts/ui/HUD.gd | Rank promotion sound in _on_rank_up() | Done |
| scripts/menus/MainMenu.gd | Removed local music player; calls stop_game_music() + play_menu_music() in _ready() | Done |
| scripts/menus/Settings.gd | Removed fullscreen toggle; added three styled HSlider controls (Master, Music, SFX); _style_slider() helper generates neon styling at runtime | Done |
| scenes/menus/Settings.tscn | Removed RowFullscreen; added RowMusicVolume and RowSFXVolume with HSlider nodes; spacing tuned to 50px uniform gap | Done |
| scripts/entities/EnemyMothership.gd | Enemy shot sound in _shoot_at(); ambient Mothership.mp3 loop in _ready() | Done |
| scripts/entities/EnemyUnit.gd | Ambient Alien Ship.wav loop in _ready() | Done |
| scripts/entities/Asteroid.gd | Asteroid Hit.mp3 in hit() | Done |
| scripts/entities/Crystal.gd | Crystal Pickup.mp3 in _on_body_entered() | Done |
| scripts/entities/PowerUp.gd | Power Up Pick UP.mp3 in _apply(); nuke add_child changed to call_deferred | Done |
| scripts/entities/NukeExplosion.gd | Nuke Exp.mp3 in _ready() | Done |
| scripts/entities/Player.gd | Shield sound in activate_shield(); velocity renamed to fly_vel to fix shadowing warning | Done |
| scripts/menus/CharacterShop.gd | HoloTill Chime.wav on successful ship purchase | Done |
| Sounds/Game Music/ (folder) | 18 tracks renamed from long descriptive names to Track 1.wav โ Track 18.wav | Done |
Remaining SFX Wired
Four new sound files identified in the Sounds folder were confirmed and wired in.
| Sound File | Trigger | Script |
|---|---|---|
| error-pop.mp3 | Failed ship purchase (insufficient coins) | CharacterShop.gd ยท _on_tile_pressed() else branch |
| sci fi alarm.mp3 | Next-wave red warning banner appears | GameScene.gd ยท _show_wave_warning() |
| Level Complete.mp3 | Player completes the level | GameScene.gd ยท _level_complete() |
| achievement.mp3 | Achievement unlocked popup fires | GameManager ยท play_achievement() called by show_achievement() |
The remaining audio files on disk (explosion-36210.mp3, futuristic-ship-ambience-494000.mp3, Powerup.mp3, PTModelSound_ID193943.mp3, Rank Up.wav, sci-fi-explosion-09-190268.mp3, science_fiction_spaceship...mp3, Spaceshuttle_Rocket_Steady_LV.mp3) were reviewed and confirmed as unused, with no game event to assign them to at this stage.
Achievements Menu: UI Rebuild
Change The Achievements screen was rebuilt from scratch to match the Settings menu visual style. Consistency across all menus is a core UI goal; every screen should feel like it belongs to the same game.
What Changed
- Removed the old
StyleBoxFlatflat-blue panel and star parallax background - Added the same
window_whole_alpha.pngpanel frame used in Settings - Same space background (
background_01_parallax_01.png) and planet decorations (top-left and bottom-right) as Settings - “ACHIEVEMENTS” title in identical neuropol font, size 48, same colour as “SETTINGS”
- Scrollable achievement list fills the frame content area
- Achievement name font size increased 22โ28, description 16โ20, row height 72โ88px for readability
Navigation Consistency
Both Settings and Achievements previously used a centred CLOSE button at the bottom of the panel. This was replaced on both screens with a โ BACK button in the top-left corner (offset 20, 20, identical to CharacterShop and LevelSelect). All four screens now share the same navigation pattern.
MainMenu: BtnAchievements Label Fix
Fix The hover label beneath the Achievements icon button was visually clipping the trailing “s” from “Achievements”, displaying “Achievement”. Root cause: the button is 200px wide and font size 22 left no margin for the 12-character word. Fixed by reducing the font size from 22 to 18, which gives the text comfortable room within the button bounds.
Achievement Unlock Popup
New A global achievement popup system was added to GameManager. Calling show_achievement("Achievement Name") from anywhere in the game triggers a full animated notification without requiring any scene setup.
Behaviour
achievement.mp3plays immediately on triggerwindow_top.pngbar slides up from off-screen bottom with a springy ease-out over 0.4 s- Text reads “ACHIEVEMENT UNLOCKED” for 1.5 s
- Text swaps to the achievement name for a further 1.5 s
- Bar slides back off-screen with ease-in over 0.35 s, then the CanvasLayer is freed
Architecture
- Creates a temporary
CanvasLayerat layer 50, renders above all game UI and menus - Popup
Controlis 1920ร260px;window_top.pngfills it proportionally - Label top offset 40px pushes text into the visual centre of the bar graphic
- Font: neuropol 32px, colour
Color(1.0, 0.95, 1.0)(soft white-pink) - All nodes self-clean via
layer.queue_freeat the end of the tween sequence
Debug
Press F6 during gameplay (debug builds only) to fire a test popup with the text “Asteroid Slayer”. Added alongside the existing F5 (Level Complete) shortcut in GameScene._unhandled_input().
## Usage โ call from anywhere:
GameManager.show_achievement("First Flight")
Files Changed in Session 12
| File | Change | Status |
|---|---|---|
| scripts/autoload/GameManager.gd | Added play_achievement() and show_achievement(name), full animated popup with sound |
Done |
| scripts/game/GameScene.gd | sci fi alarm.mp3 in _show_wave_warning(); Level Complete.mp3 in _level_complete(); F6 debug shortcut for achievement popup |
Done |
| scripts/menus/CharacterShop.gd | error-pop.mp3 on failed purchase (insufficient coins) | Done |
| scenes/menus/Achievements.tscn | Full rebuild, matching Settings visual style; window_whole_alpha.png frame, space background, planet decorations, BtnBack top-left | Done |
| scripts/menus/Achievements.gd | Updated node paths for new scene structure; font sizes increased; row height increased | Done |
| scenes/menus/Settings.tscn | Replaced centred BtnClose with BtnBack in top-left (offset 20, 20) | Done |
| scripts/menus/Settings.gd | Updated btn_close node path to $BtnBack |
Done |
| scenes/menus/MainMenu.tscn | BtnAchievements font size 22โ18 to fix “Achievement” label clipping | Done |
Settings Controls Diagram
New A controls reference section was added to the Settings menu below the volume sliders, showing the keyboard layout for movement and firing.
Layout
The diagram is built entirely in code via _build_controls() called from _ready(), keeping the .tscn clean. A ControlsSection VBoxContainer node was added to Settings.tscn as the anchor point; its position was tuned in the editor to sit neatly beneath the sound rows.
| Key(s) | Action |
|---|---|
| W / A / S / D or โ โ โ โ | Move ship |
| Space | Fire |
Key Box Styling
Each key is rendered as a PanelContainer with a StyleBoxFlat: dark navy background (Color(0.02, 0.08, 0.20)), 2px cyan border, 6px corner radius, matching the existing UI colour scheme. The SPACE bar uses a wider minimum size (200px) to visually distinguish it as a bar key. The “or” connector between WASD and arrow clusters is rendered in a muted grey to reduce visual weight.
Why it’s invisible in the editor
The controls diagram is invisible in the Godot scene editor because _build_controls() runs in _ready(), which only executes at runtime. In the editor the ControlsSection node exists but is empty. This is the same behaviour as the slider styling; it is expected and not a bug.
Settings BtnMainMenu Not Working
Fix The Main Menu button in the Settings screen had no effect when pressed. The BtnMainMenu node existed in Settings.tscn but was never referenced or connected in Settings.gd.
Fixed by adding the @onready reference and connecting it to a new _go_main_menu() function:
func _go_main_menu() -> void:
_gm.play_ui_click()
queue_free()
get_tree().change_scene_to_file("res://scenes/menus/MainMenu.tscn")
The Settings overlay is freed before the scene change so it does not linger in memory.
Planned Game Over Screen Redesign
The current Game Over panel lives inside HUD.tscn‘s CanvasLayer alongside the score widget, health bar, and pause button. Because they share the same layer these HUD elements render through the game over panel making the screen look cluttered.
Proposed Fix
Extract Game Over into its own dedicated scene (scenes/menus/GameOver.tscn) on a higher CanvasLayer, the same approach as how Settings overlays the game. The scene will use the same full-screen background style as all other menus (space background, planet decorations, window_whole_alpha.png frame). When game over fires:
- HUD hides its live elements (LeftWidget, PauseButton)
GameOver.tscnis instantiated at a higher layer, rendering cleanly on top- Retry / Main Menu / Ships buttons sit inside the panel frame
This completely eliminates the see-through issue and brings the Game Over screen into visual consistency with all other menus. Implementation pending confirmation.
Files Changed in Session 13
| File | Change | Status |
|---|---|---|
| scenes/menus/Settings.tscn | Added ControlsSection VBoxContainer node; position tuned in editor to sit below sound rows |
Done |
| scripts/menus/Settings.gd | Added _build_controls(), _make_key(), _make_action_label() for programmatic keyboard diagram; wired BtnMainMenu to new _go_main_menu() |
Done |
Release Readiness Review
A comprehensive release readiness review was conducted with the goal of shipping on Steam and recovering the ยฃ100 listing fee. All dev logs (Sessions 1โ13) were audited to answer open questions and surface gaps. Key findings below.
Platform Decision
Steam first. Mobile (App Store / Google Play) only if Steam sales cover the additional listing fee. This defers AdMob, IAP, GDPR, and mobile joystick work entirely.
Ship Mechanics Confirmed
- Viper: Fast nimble fighter: rotation 300, thrust 3000, max speed 750, friction 0.85, zero drift. Snappy turns, no slide.
- BGS-1 / BGS-2: Slow capital ships: rotation 50, thrust 600, max speed 260, friction 0.90. Always-firing 3 front guns plus 6 side guns per side unlocked progressively by ship level. Side guns fire at 90ยฐ to heading. 13-blast death sequence.
- Gap: BGS standalone level-up benefit not implemented. Viper level-up benefit not designed.
Level Design
99 levels are algorithmically scaled, not hand-authored. Wave composition (asteroid count, spawn interval) increases with level number. Enemy units enter at level 4; mothership at level 21; dual mothership at level 41. Mid-game (levels 30โ70) has not been validated for feel, and that is the biggest unknown.
Known Blockers Before Shipping
DEBUG_SHIP_SCALEmust be set tofalse- DEBUG_AddCoins button must be removed from HUD
- F6 achievement popup debug shortcut is gated behind
OS.is_debug_build(), safe to leave - Gun barrel positions need visual verification per ship variant
- Collision polygons: 18 ship variants still need hull geometry
- Full clean end-to-end playthrough must happen before submission
- Game Over screen see-through issue must be fixed
- Steam store assets: capsule image, header, minimum 5 screenshots, trailer, description, tags: none exist yet
- Privacy policy required by Steam
First-Run Tutorial Overlay
New A one-time tutorial overlay was added to GameScene. It appears automatically the first time a new player starts Level 1 and is never shown again after dismissal. Tested and confirmed working.
Behaviour
- Shows only on
current_level == 1and only whentutorial_seen == false, never shown in endless mode - Blocks wave start until the player actively dismisses it (any key or mouse button)
- On dismiss: sets
tutorial_seen = true, saves to disk, calls_start_wave() - After dismissal the flag persists in the save file, so the overlay never appears again for that player
Visual Design
The overlay uses the same key-box visual language as the Settings controls diagram (dark navy background, 2px cyan border, 6px corner radius) so the style is consistent across both places a player sees the controls.
- Dark semi-transparent backdrop (
Color(0, 0, 0, 0.72)) covers the full screen and blocks mouse input passing through - Centred card panel with dark navy background and cyan border
- “HOW TO PLAY” title in neuropol 42px
- WASD cluster + “or” + arrow cluster โ “Move”
- Wide SPACE bar โ “Fire”
- Wide ESC bar โ “Pause”
- “PRESS ANY KEY TO BEGIN” blinking on a looping tween (opacity 1.0 โ 0.2 โ 1.0, 0.55 s each leg)
Architecture
tutorial_seen: booladded toGameManagerpersistent data, saved and loaded undersettings/tutorial_seeninuser://save.cfg_tutorial_layer: CanvasLayervar onGameScene, non-null while overlay is active- Overlay lives at layer 30, above the game and below the achievement popup (layer 50)
_show_tutorial_overlay()builds all nodes programmatically using two helpers:_tut_make_key()and_tut_make_action_label()_dismiss_tutorial()frees the layer, sets the flag, saves, then calls_start_wave()_unhandled_input()catches any key/mouse press while_tutorial_layer != null, calls_dismiss_tutorial(), and returns, so pause/debug input is unaffected after dismissal
tutorial_seen key is absent from all existing save files. load_game() uses cfg.get_value("settings", "tutorial_seen", false), so all existing players will see the tutorial once on their next session, exactly the intended behaviour. Files Changed in Session 14
| File | Change | Status |
|---|---|---|
| scripts/autoload/GameManager.gd | Added tutorial_seen: bool = false; saved and loaded under settings/tutorial_seen |
Done |
| scripts/game/GameScene.gd | Added _tutorial_layer var; conditional tutorial check in _ready(); _show_tutorial_overlay(), _tut_make_key(), _tut_make_action_label(), _dismiss_tutorial(); tutorial dismiss handling in _unhandled_input() |
Done |
Game Over Screen as a Dedicated Scene
NewChange The Game Over panel was extracted from HUD.tscn into its own dedicated scene, scenes/menus/GameOver.tscn. This completely resolves the HUD see-through issue where the score widget, health bar, and pause button were visible behind the panel.
Root Cause of the Old Problem
The old GameOverPanel and GameOverOverlay were siblings of the HUD score/health elements inside the same CanvasLayer. Because they shared a layer, the semi-transparent overlay could never fully hide the other elements; they all rendered at the same depth.
Solution
The Game Over screen is now instantiated as its own CanvasLayer at layer 15, above the HUD layer, from GameScene._on_player_died(). The full-screen space background (background_01_parallax_01.png) is fully opaque, so no HUD elements are visible underneath. No overlay hack required.
Visual Design
Matches all other menus: identical background, planet decorations, and window_whole_alpha.png panel frame:
- Full-screen
background_01_parallax_01.png, same as Settings and Achievements - Semi-transparent Dimmer
ColorRectadded by user in editor for depth - Planet decorations top-left and bottom-right
window_whole_alpha.pngframe centred (351โ1597, 16โ1041, identical to Settings)- “GAME OVER” title, Score label, Best label inside the frame
- Continue button (hidden by default, shown only if the player has โฅ 100 coins on desktop)
- Retry button
- Main Menu icon button, top-right
- Settings icon button, bottom-right (added in the editor)
- Ships icon button, bottom-left
Architecture
# GameScene._on_player_died()
func _on_player_died() -> void:
is_game_over = true
_gm.play_sfx(preload("res://Sounds/Spaceship-large-underwater-explosion.mp3"))
await get_tree().create_timer(1.0).timeout
var go: CanvasLayer = load("res://scenes/menus/GameOver.tscn").instantiate()
add_child(go)
go.setup(_gm.session_score, _gm.best_score, _gm.coins)
get_tree().paused = true
GameOver.gd has process_mode = PROCESS_MODE_ALWAYS so buttons remain interactive while the scene tree is paused. All button handlers unset get_tree().paused before any scene change.
HUD Cleanup
All game over code was removed from HUD.tscn and HUD.gd: GameOverPanel, GameOverOverlay, all their child nodes, the five StyleBox sub-resources, all @onready refs, show_game_over(), and the four button handler functions. HUD is now purely a gameplay overlay: score, health, rank badge, pause button, mobile controls.
Files Changed in Session 15
| File | Change | Status |
|---|---|---|
| scenes/menus/GameOver.tscn | New scene: space background, dimmer, planet decorations, window_whole_alpha.png frame, GAME OVER title, Score/Best labels, Continue/Retry/MainMenu/Settings/Ships buttons. Layout adjusted by user in editor. | Done |
| scripts/menus/GameOver.gd | New script: setup(score, best, coins), handlers for all five buttons including Settings overlay and Ships navigation |
Done |
| scenes/ui/HUD.tscn | Removed GameOverOverlay, GameOverPanel and all children; removed all related StyleBox sub-resources and ext_resource references | Done |
| scripts/ui/HUD.gd | Removed all game_over_* node refs, show_game_over(), four button handlers, CONTINUE_COST const; HUD is now gameplay-only | Done |
| scripts/game/GameScene.gd | _on_player_died() now instantiates GameOver.tscn, calls setup(), sets get_tree().paused = true |
Done |
Achievement System: Full Implementation
New All 12 achievements are now fully wired. The popup and sound already existed; this session added the unlock logic, lifetime stat tracking, persistence, and live display in the Achievements screen.
Design Decisions Confirmed
- Coin Hoarder triggers on lifetime coins earned (not current balance), so spending does not reset progress
- Asteroid Slayer counts all asteroid destructions including split fragments (every
_on_asteroid_destroyedevent) - Endless Pilot: 10 waves in a single consecutive endless run; counter resets on death/scene reload
New Data in GameManager
Four lifetime counters added to persistent data, saved under a new [stats] section in user://save.cfg:
| Variable | Drives | Incremented In |
|---|---|---|
| total_asteroids_destroyed | Asteroid Slayer (โฅ 100) | GameScene._on_asteroid_destroyed() |
| total_crystals_collected | Crystal Collector (โฅ 50) | Crystal._on_body_entered() |
| total_nukes_used | Nuke ’em (โฅ 10) | GameManager.use_nuke() |
| total_coins_earned | Coin Hoarder (โฅ 5,000) | GameManager.add_coins() |
A fifth new field, unlocked_achievements: Dictionary, is saved under [achievements]. It is keyed by achievement name (string) with value true. Existing save files default to an empty dictionary, so all achievements start locked for current players.
Central Unlock Method
func unlock_achievement(achievement_name: String) -> void:
if unlocked_achievements.get(achievement_name, false):
return # already unlocked โ never fire twice
unlocked_achievements[achievement_name] = true
save_game()
show_achievement(achievement_name)
The one-time guard means it is safe to call on every score add, every coin add, every wave; it only acts the first time the condition is met.
Trigger Map
| Achievement | Condition | Trigger Location |
|---|---|---|
| First Flight | completed_level == 1 | GameScene._level_complete() |
| Asteroid Slayer | total_asteroids_destroyed โฅ 100 | GameScene._on_asteroid_destroyed() |
| Crystal Collector | total_crystals_collected โฅ 50 | Crystal._on_body_entered() |
| Coin Hoarder | total_coins_earned โฅ 5,000 | GameManager.add_coins() |
| Promoted! | current_rank โฅ 4 (Officer 1) | GameManager._check_rank_up() |
| Ship Upgrade | new_level โฅ 1 (X2) | GameScene._on_ship_leveled_up() |
| Max Firepower | new_level โฅ 2 (X3) | GameScene._on_ship_leveled_up() |
| Fleet Commander | all owned_ships == true | GameManager.buy_ship() |
| Sharpshooter | player_health == 3 on level complete | GameScene._level_complete() |
| Nuke ’em | total_nukes_used โฅ 10 | GameManager.use_nuke() |
| Endless Pilot | _endless_waves_completed โฅ 10 | GameScene._next_wave_or_complete() |
| High Scorer | session_score โฅ 100,000 | GameManager.add_score() |
Achievements Screen: Live Data
Achievements.gd was updated to read unlock state from GameManager.unlocked_achievements at build time. The PLACEHOLDER_ACHIEVEMENTS const (which hardcoded "unlocked": false for all entries) was replaced with ACHIEVEMENTS, names and descriptions only. Each row now checks _gm.unlocked_achievements.get(name, false) to determine its star colour and label colour. Locked achievements show a hollow grey star (โ) and white-blue name; unlocked achievements show a filled gold star (โ
) and gold name.
_ready() and trigger the relevant action. Files Changed in Session 16
| File | Change | Status |
|---|---|---|
| scripts/autoload/GameManager.gd | Added 4 lifetime stat counters + unlocked_achievements dict; unlock_achievement() method; checks wired into add_coins, use_nuke, _check_rank_up, buy_ship, add_score; all new fields saved/loaded |
Done |
| scripts/game/GameScene.gd | Asteroid counter + Asteroid Slayer in _on_asteroid_destroyed; First Flight + Sharpshooter in _level_complete; Ship Upgrade + Max Firepower in _on_ship_leveled_up; Endless Pilot in _next_wave_or_complete |
Done |
| scripts/entities/Crystal.gd | Crystal counter increment + Crystal Collector check in _on_body_entered |
Done |
| scripts/menus/Achievements.gd | Replaced PLACEHOLDER_ACHIEVEMENTS with ACHIEVEMENTS (no unlocked field); added _gm ref; _build_list() reads live unlock state from _gm.unlocked_achievements |
Done |
Open Items
| Item | Notes | Status |
|---|---|---|
| Insufficient funds sound | Wired: error-pop.mp3 plays on failed shop purchase | Done |
| First-run tutorial overlay | Implemented and tested: HOW TO PLAY card blocks wave start, dismissed by any key, one-time flag saved to disk | Done |
| Game Over screen redesign | Extracted into GameOver.tscn at layer 15 with full menu-style background; HUD see-through issue eliminated | Done |
| Achievement system | All 12 achievements wired with live tracking and persistence; testing deferred to next session | Testing |
| science_fiction_spaceship_or_rocket_fly_by_fast_001.mp3 | Confirmed unused, no game event assigned | Unused |
| PTModelSound_ID193943.mp3 | Confirmed unused, purpose unknown | Unused |
| Rank Up.wav | Replaced by Firefly level-up sound, now unused | Unused |
| Shield scale per ship | Per-ship tuning in editor | Tuning |
| Collision polygons | 18 polygons need geometry authored | Pending |
| BGS rocket mechanic / Viper level-up benefit | Gameplay feature, not yet designed | Pending |
| Gun barrel positions | Visual verification needed per ship variant | Pending |
| Full end-to-end playthrough | Must happen before Steam submission; no clean run on record | Before ship |
| Steam store assets | Capsule image, header, min 5 screenshots, trailer, description, tags: none exist | Before ship |
| Privacy policy | Required by Steam | Before ship |
| Mobile joystick wiring | Deferred: Steam first | Deferred |
| Coin shop / IAP / AdMob / GDPR / leaderboards | Deferred: Steam first | Deferred |
| DEBUG_SHIP_SCALE = false | Must be set before shipping | Before ship |
| Remove DEBUG_AddCoins button | Must be removed before shipping | Before ship |