Dev Log โ 1 May 2026
Session Overview
Big session. The Pause Menu scene was rearranged between sessions, moving BtnRestart into a child of BtnMainMenu, which broke the node paths in PauseMenu.gd and caused two errors on launch. After fixing those I also cleared an integer division warning, expanded the game to 99 levels, built a per-level background system with 13 unique backgrounds, added slow circular float animation to background planets, and completely overhauled button hover detection to cover the full sprite area with consistent visual feedback.
Pause Menu: Node Not Found / Null Instance Errors
Error 1Node not found: "Panel/VBox/BtnRestart". The @onready path no longer matched the scene tree.
Error 2Invalid access to property 'pressed' on null instance, a direct consequence of Error 1. When btn_restart is null, connecting its pressed signal in _ready() immediately crashes.
Root Cause
BtnRestart was reparented from a direct child of Panel/VBox to a child of Panel/VBox/BtnMainMenu. The script still referenced the old path:
# Old โ broken after scene edit
@onready var btn_restart: Button = $Panel/VBox/BtnRestart
# New โ matches actual scene tree
@onready var btn_restart: Button = $Panel/VBox/BtnMainMenu/BtnRestart
Fix
Updated the @onready path in PauseMenu.gd to $Panel/VBox/BtnMainMenu/BtnRestart. No other changes needed; signal connections and button logic remain identical.
@onready paths are literal node paths resolved at runtime. Moving a node in the editor scene tree silently invalidates any hardcoded paths in scripts. Fix the path error first and the null-access error disappears with it. GameManager: Integer Division Warning
WarningINTEGER_DIVISION โ Integer division. Decimal part will be discarded. at GameManager.gd:172.
Root Cause
In _auto_upgrade_ship(), the ship series was calculated by dividing two integers:
var series: int = selected_ship / 3 # int รท int โ GDScript warns
GDScript 4 raises INTEGER_DIVISION when both operands are integers. The result here is mathematically correct (floor truncation) but the engine can’t know that without an explicit signal of intent.
Fix
Changed to float division with floori(), which returns an int and makes the truncation-to-floor explicit:
var series: int = floori(selected_ship / 3.0) # explicit float div โ no warning
floori() always rounds toward negative infinity. Since selected_ship is always โฅ 0, the result is identical to the original, just without the warning.
Pause Menu: Spinning Button Animation
Added the same hover/tap spin animation used on the Main Menu to all three Pause Menu buttons that have a sprite icon child.
| Button | Spinning sprite path |
|---|---|
| Resume | Panel/VBox/BtnResume/MainButton1/Playbutton |
| Settings | Panel/VBox/BtnSettings/MainButton2/SettingsButton |
| Restart | Panel/VBox/BtnMainMenu/BtnRestart/MainButton2/Playbutton |
Each button triggers a 360ยฐ rotation (TAU) over 0.5 s with sine ease in/out on mouse_entered and button_down. A per-button guard flag prevents re-triggering while the spin is still running. BtnMainMenu has no sprite child so it was left without a spin.
Warning & Error Sweep: Five Issues Resolved
Five separate warnings and errors in Godot’s output were resolved in one pass:
1. PowerUp.tscn: Invalid Script UID
Warningext_resource, invalid UID: uid://cpowerup0001b โ using text path instead. The UID stored in the scene file for PowerUp.gd did not match the UID Godot has on disk. Fixed by removing the uid= attribute from the ext_resource entry, and Godot resolves the script by path with no mismatch and no warning.
2. GameScene.gd:154: Unused Variable
WarningUNUSED_VARIABLE โ "total" is declared but never used. The variable summed the three asteroid counts but was never read. Removed the unused declaration.
3. GameScene.gd:174: Integer Division
WarningINTEGER_DIVISION on (level - 1) / 3. Same class of issue as the GameManager.gd fix above. Changed to floori((level - 1) / 3.0).
4. Asteroid.gd:195: Shadowed Variable
WarningSHADOWED_VARIABLE โ "hit" is shadowing an already-declared function at line 147. The public hit() method and a local var hit: bool inside _integrate_forces() shared the same name. Renamed the local boolean to _bounced throughout; the leading underscore also signals it is a purely internal variable.
5. Settings.gd:24 & 29: Audio Bus Index Out of Bounds
ErrorIndex p_bus = -1 is out of bounds when toggling Music or SFX. AudioServer.get_bus_index("Music") returns -1 when no bus with that name exists. Fixed with an index guard:
var bus := AudioServer.get_bus_index("Music")
if bus >= 0:
AudioServer.set_bus_mute(bus, not on)
The toggle still saves the preference to disk; it just skips the mute call until “Music” and “SFX” buses are added in Godot’s Audio panel.
99 Levels: Full Level Progression
Expanded the game from 24 levels to the full 99. All 99 level icon assets (Game Assests/UI/Levels/Level 1.png โ Level 99.png) were already in place. Four files had hardcoded caps that needed updating:
| File | Change |
|---|---|
| GameManager.gd | level_stars.resize(24) โ resize(99) |
| GameScene.gd | current_level < 24 โ < 99 |
| LevelComplete.gd | next <= 24 โ <= 99 |
| LevelSelect.gd | TOTAL_LEVELS = 24 โ 99 |
The Level Select screen already had pagination infrastructure built in (BtnPrev, BtnNext, PageLabel). With TOTAL_LEVELS = 99 and LEVELS_PER_PAGE = 24, it now shows 5 pages automatically, with no structural changes needed.
Level Select: Out of Bounds Crash on Page 2+
ErrorOut of bounds get index '24' (on base: 'Array[int]') in is_level_unlocked() when navigating to page 2 of the Level Select.
Root Cause
The existing save file had level_stars stored as a 24-entry array. When load_game() restored it, it overwrote the 99-entry default set by _init_save_defaults(). Any call to is_level_unlocked(26) then tried to access level_stars[24], one past the end.
Fix in Two Parts
Fix 1 In load_game(), resize the array after loading if it came from an older save:
level_stars = cfg.get_value("player", "level_stars", level_stars)
if level_stars.size() < 99:
level_stars.resize(99)
Fix 2 Added a bounds guard to is_level_unlocked() as a permanent safety net:
func is_level_unlocked(level: int) -> bool:
if level == 1:
return true
var idx: int = level - 2
if idx < 0 or idx >= level_stars.size():
return false
return level_stars[idx] > 0
LEVELS_PER_PAGE was also corrected back to 24; it had been accidentally changed to 25, putting 25 icons on page 1 instead of the intended 24.
Per-Level Background System: 13 Unique Backgrounds
Implemented a per-level background system using separate editable scene files. The previous hardcoded background sprite was replaced with a BackgroundRoot Node2D placeholder in GameScene.tscn. Each level now loads its own background scene at runtime.
How It Works
A LEVEL_BG constant array (100 entries, index 0 unused) in GameScene.gd maps every level 1โ99 to one of 13 background scene paths. At startup, _load_level_background() reads the current level, loads the matching scene, and instances it into $BackgroundRoot:
func _load_level_background() -> void:
var level: int = _gm.current_level
if level < 1 or level >= LEVEL_BG.size():
return
var bg_path: String = LEVEL_BG[level]
if bg_path.is_empty() or not ResourceLoader.exists(bg_path):
return
var bg_scene: PackedScene = load(bg_path)
if bg_scene == null:
return
var bg: Node2D = bg_scene.instantiate()
$BackgroundRoot.add_child(bg)
Background Scene Structure
Each BG_XX.tscn has a Node2D root with z_index = -10 and z_as_relative = false (ensuring it renders behind the star parallax layer at z=-5 regardless of tree position). Children are a centred background Sprite2D plus up to three planet sprites placed in corners, fully editable in the Godot editor.
Background Assignment
| Background | Levels | Space Image | Planets |
|---|---|---|---|
| BG_01 | 1โ8 | ID154193 (baked planets) | None (planets baked into image) |
| BG_02 | 9โ16 | ID154212 | ID9256 top-left, ID61669 bottom-right |
| BG_03 | 17โ24 | ID154212 | ID9256 top-right, ID54710 bottom-left, ID60912 bottom-right |
| BG_04 | 25โ32 | ID154242 | ID54700 top-left, ID9245 bottom-right |
| BG_05 | 33โ40 | ID154242 | ID54700 bottom-left, ID9245 top-right, ID60912 bottom-right |
| BG_06 | 41โ48 | ID154261 | ID60912 top-left, ID9256 bottom-right |
| BG_07 | 49โ56 | ID154261 | ID60912 top-right, ID61669 bottom-left, ID54710 top-left |
| BG_08 | 57โ64 | ID60890 | ID9245 top-left, ID54700 bottom-right |
| BG_09 | 65โ72 | ID60890 | ID9245 bottom-left, ID54700 top-right, ID61669 top-left |
| BG_10 | 73โ80 | ID61648 | ID9256 top-right, ID60912 bottom-left |
| BG_11 | 81โ88 | ID61648 | ID61669 top-left, ID60912 bottom-right, ID54700 bottom-left |
| BG_12 | 89โ96 | ID54678 | ID9256 top-left, ID61669 bottom-right |
| BG_13 | 97โ99 | ID54678 | ID9256 bottom-right, ID9245 top-left, ID54710 top-right |
scenes/backgrounds/BG_XX.tscn in the Godot editor to reposition or resize planets. PTModelSprite_ID61659 was referenced in planning but does not exist on disk, so I substituted available planets. Planet Float Animation: Slow Circular Drift
Added a subtle ambient animation to all background planet sprites. Each planet drifts in a slow circular path around its resting position. sin() drives X and cos() drives Y at the same frequency, producing a smooth oval orbit.
| Property | Value | Effect |
|---|---|---|
amplitude |
12 px | Drift radius: how far the planet moves |
speed |
0.1 rad/s | ~63 s per full orbit, very slow and dreamy |
phase |
0.0 / 2.09 / 4.19 | 120ยฐ offsets per planet so they don’t move in lockstep |
The script is attached directly to each planet Sprite2D node across BG_02โBG_13. BG_01 has no extra planets; its planets are baked into the background texture. All properties are @export and adjustable per-planet in the Inspector.
Pause Menu: Icon Node Paths Broken After Scene Edit
Icon sprites on the Settings and Restart buttons were swapped mid-session. Two @onready paths broke because the new icon nodes have different names:
| Button | Old node name | New node name |
|---|---|---|
| Settings | SettingsButton |
IconsOnButtonsCopy28 |
| Restart | Playbutton |
IconsOnButtonsCopy19 |
Both @onready declarations in PauseMenu.gd updated to match the new names. Rule: always read the .tscn first to confirm the actual node name before editing the script.
Button Hover: Spin and Highlight on Full Icon Area
Hovering over the visible button sprites was not triggering the spin animation or any visual feedback. Two root causes:
- Hit area mismatch: the
ButtonControl rect does not always match the full visual bounds of the associatedSprite2D. Sprite2D children can be positioned partially outside the button’s Control rect. Hovering those pixels never firesmouse_entered. - No visual hover state: all button states use
StyleBoxEmpty, so even when the button IS hovered, there is no built-in visual change.
Fix
Replaced button.mouse_entered signal connections with a _process loop that checks the mouse position directly against the background sprite’s rect using Godot’s local-space projection:
var over: bool = _play_bg.get_rect().has_point(_play_bg.to_local(mouse))
if over and not _play_hovered:
_play_hovered = true
_play_bg.modulate = Color(1.3, 1.3, 1.3) # brighten sprite on hover
_spin_play_button()
elif not over and _play_hovered:
_play_hovered = false
_play_bg.modulate = Color.WHITE # reset on exit
This makes the entire visible sprite graphic the hover target. button_down connections are kept for mobile touch. Applied to MainMenu.gd (Play, Endless) and PauseMenu.gd (Resume, Settings, Restart).
Button Hover: Click and Parse Error Fixes
Two further issues found after the initial hover fix:
1. Parse Error: get_global_mouse_position() Not Found
ErrorPauseMenu.gd:43 - Parse Error: Function "get_global_mouse_position()" not found in base self.
CanvasLayer extends Node, not CanvasItem, so get_global_mouse_position() does not exist on it. Fixed by switching to get_viewport().get_mouse_position(), which is available from any node and returns the same viewport-space coordinates.
2. Clicking the Icon Did Not Fire the Button Action
The sprite area extends beyond the Button‘s Control rect. Clicks landing on the sprite but outside the rect never reached the button’s pressed signal. Fix: added _input() to both scripts that checks the click position against the sprite rect and calls the action function directly. The pressed signal connection was removed for sprite-backed buttons to avoid double-fire:
func _input(event: InputEvent) -> void:
if not (event is InputEventMouseButton and event.pressed
and event.button_index == MOUSE_BUTTON_LEFT):
return
var mouse: Vector2 = get_viewport().get_mouse_position()
if _resume_bg.get_rect().has_point(_resume_bg.to_local(mouse)):
_resume()
elif _settings_bg.get_rect().has_point(_settings_bg.to_local(mouse)):
_open_settings()
elif _restart_bg.get_rect().has_point(_restart_bg.to_local(mouse)):
_restart()
Button Hover: Full Visual Consistency Across Sprite and Text Areas
Two inconsistencies remained after the initial hover fix: hovering the button text area showed a default light-grey box with white text while the sprite hover area showed no box. The goal: identical appearance everywhere (grey box, cyan text, sprite brightens, spin) regardless of where the mouse is.
Approach
A single StyleBoxFlat created in _ready() is used for both hover paths:
_hover_box = StyleBoxFlat.new()
_hover_box.bg_color = Color(0.22, 0.22, 0.22, 0.85)
_hover_box.set_corner_radius_all(5)
# Native button hover (mouse over button text rect):
btn.add_theme_stylebox_override("hover", _hover_box)
btn.add_theme_color_override("font_hover_color", HOVER_COLOR)
# Sprite hover (mouse over sprite, detected in _process):
btn.add_theme_stylebox_override("normal", _hover_box) # apply same box
btn.add_theme_color_override("font_color", HOVER_COLOR) # apply same text colour
# On exit โ remove both overrides, box returns to normal state
The hover colour was changed from gold (Color(1.0, 0.9, 0.3)) to the game’s system UI accent blue (Color(0, 0.95, 1)), matching the colour used on the HUD score label, Level Select page label, and Level Complete headers.
Result
| Area Hovered | Grey Box | Text Colour | Sprite | Spin |
|---|---|---|---|---|
| Sprite graphic | โ (normal override) | Cyan | Brightens ร1.3 | โ |
| Button text area | โ (native hover style) | Cyan (font_hover_color) | Brightens ร1.3 | โ |
| Neither | โ | Default | White | โ |
Applied to MainMenu.gd (Play, Endless) and PauseMenu.gd (Resume, Settings, Restart).
Open Items
| Item | Priority | Notes |
|---|---|---|
| Planet positions BG_02โBG_13 | High | Placeholder corners at scale 0.5; needs editor tuning |
| Add “Music” and “SFX” audio buses | Medium | Required to activate Settings mute toggles |
| Mobile joystick wiring | Medium | Not connected to player movement |
| HUD nuke count display | Medium | update_nukes() stub still returns pass |
| Nuke activation mechanic | Medium | Stored in nukes_held, no key binding yet |
| Endless mode difficulty scaling | Medium | Currently stays at level-0 speed forever |
| Level Complete visual polish | Low | Polish pass still needed |
| Coin shop / IAP | Low | Not started |
| AdMob, Vungle, AppLovin | Low | SDK wiring pending |
| GDPR consent | Low | Required before store submission |
| Google Play Games leaderboards | Low | Not started |