Asteroids Destroyer Dev Log – 1 May 2026

Asteroids Destroyer โ€” Development Journal

Dev Log โ€” 1 May 2026

๐Ÿ“… Session 1
๐ŸŽฎ Godot 4.6
๐ŸŒŒ Background System
๐Ÿ–ฑ๏ธ Hover Overhaul
๐Ÿ› Bug Fixes
๐Ÿ“‹

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.

Rule: Godot’s @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
Note: Planet positions and scale are placeholder defaults (corners, scale 0.5). Open any 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.

Tuning note: Speed was reduced from the initial 0.8 rad/s (~8 s orbit) to 0.1 rad/s (~63 s orbit) after the first test showed movement was too fast and distracting.
โธ๏ธ

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:

  1. Hit area mismatch: the Button Control rect does not always match the full visual bounds of the associated Sprite2D. Sprite2D children can be positioned partially outside the button’s Control rect. Hovering those pixels never fires mouse_entered.
  2. 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