Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8a26313
[BUG] Orphan Node Leak from Placeholder Sprites in main_scene.gd #540
ikostan Apr 16, 2026
faa58f0
[QA] Orphan Node Leak Fix – Test Plan #549
ikostan Apr 16, 2026
4782dba
Update test/gut/test_main_scene_orphan_nodes.gd
ikostan Apr 16, 2026
49ac64a
Update main_scene.gd
ikostan Apr 16, 2026
7bfa831
Add orphan leak helper and update tests
ikostan Apr 16, 2026
1bfd05f
Update main_scene.gd
ikostan Apr 16, 2026
2a647aa
suggestion: Consider tightening the type of stats_panel instead of us…
ikostan Apr 16, 2026
3713757
Update test_main_scene_orphan_nodes.gd
ikostan Apr 16, 2026
437369f
Update test_player_lifecycle.gd
ikostan Apr 16, 2026
c36a488
Update main_scene.gd
ikostan Apr 16, 2026
5822274
Update test_player_lifecycle.gd
ikostan Apr 16, 2026
608020e
[FEATURE] Randomize rotation for organic decor sprites in main scene …
ikostan Apr 16, 2026
8eb7436
Update main_scene.gd
ikostan Apr 16, 2026
237b33b
Make the background look as random and organic as possible
ikostan Apr 16, 2026
4d999f4
Update main_scene.gd
ikostan Apr 16, 2026
95b2b9e
[FEATURE] Increase parallax background chunk size to prevent visual r…
ikostan Apr 16, 2026
d63252e
Add GUT tests for parallax chunk sizes
ikostan Apr 16, 2026
26f42a0
GUT test file dedicated specifically to verifying the randomized tran…
ikostan Apr 16, 2026
f38072c
Adjust bushes/decor layer height and density
ikostan Apr 16, 2026
cb04302
assertions that lock in the "Goldilocks Zone"
ikostan Apr 16, 2026
f7df782
Update test_main_scene_parallax_chunks.gd
ikostan Apr 16, 2026
221cbd6
Update scripts/main_scene.gd
ikostan Apr 16, 2026
60e24c2
Update main_scene.gd
ikostan Apr 16, 2026
909d4a1
Merge branch 'orphan-node-leak-from-placeholder-sprites-in-main_scene…
ikostan Apr 16, 2026
aaddd37
Update test_decor_layer_transformations.gd
ikostan Apr 16, 2026
7127f19
Rename performance test & add parallax checks
ikostan Apr 16, 2026
3740a4c
Improve test teardown and prevent orphan nodes
ikostan Apr 16, 2026
deb1ae2
Performance assertion is likely to be CI-flaky, and _process side eff…
ikostan Apr 16, 2026
2588460
suggestion: The hardcoded screens_tall = 8.0 is duplicated and would …
ikostan Apr 16, 2026
2f94791
Update gut_test_helper.gd
ikostan Apr 16, 2026
a1c6821
Update test_main_scene_parallax_and_performance.gd
ikostan Apr 16, 2026
b9cfc17
Use GutHelper.safe_hard_free in tests
ikostan Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions scripts/main_scene.gd
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ var _showing_unbound_warning: bool = false
var _showing_unbound_key_message: bool = false

@onready var player: Node2D = $Player
# @onready var stats_panel: Panel = $PlayerStatsPanel
@onready var stats_panel: Variant = $PlayerStatsPanel
@onready var stats_panel: Panel = $PlayerStatsPanel
@onready var background: ParallaxBackground = $Background
@onready var bushes_layer: ParallaxLayer = $Background/Bushes # Reference to the bushes layer
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
@onready var decor_layer: ParallaxLayer = $Background/Decor # Reference to the decor layer
Expand Down Expand Up @@ -125,13 +124,17 @@ func setup_bushes_layer(viewport: Vector2) -> void:
var bush_ids: Array = Array(texture_preloader.get_resource_list()).filter(
func(id: String) -> bool: return id.begins_with("bush_")
)
print("Loaded ", bush_ids.size(), " bush textures")

if bush_ids.is_empty():
return

var num_bushes: int = bush_ids.size()
var layer_height: float = viewport.y * 4
# THE GOLDILOCKS ZONE:
# 8 screens is the sweet spot for infinite illusion vs CPU overhead
var screens_tall: float = 8.0
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
var layer_height: float = viewport.y * screens_tall

# Drop density multiplier to match
var num_bushes: int = bush_ids.size() * 2

for i in range(num_bushes):
var bush: Sprite2D = Sprite2D.new()
Expand All @@ -152,14 +155,14 @@ func setup_bushes_layer(viewport: Vector2) -> void:
bushes_layer.motion_mirroring = Vector2(0, layer_height)


## Sets up the decor layer with random X positions, sizes, and textures.
## Sets up the decor layer with random X positions, sizes, textures, rotations, and flips.
## @param viewport: Vector2 - The viewport size.
## @return: void
func setup_decor_layer(viewport: Vector2) -> void:
if not decor_layer:
return

# Clear existing children
# Clear existing children (Instantly detach, then safely queue)
for child in decor_layer.get_children():
decor_layer.remove_child(child)
child.queue_free()
Expand All @@ -168,13 +171,20 @@ func setup_decor_layer(viewport: Vector2) -> void:
var decor_ids: Array = Array(texture_preloader.get_resource_list()).filter(
func(id: String) -> bool: return id.begins_with("decor_")
)
print("Loaded ", decor_ids.size(), " decor textures")

if decor_ids.is_empty():
return

var num_decors: int = decor_ids.size()
var layer_height: float = viewport.y * 4
# THE GOLDILOCKS ZONE:
# Match the bushes layer height
var screens_tall: float = 8.0
var layer_height: float = viewport.y * screens_tall

# Drop density multiplier to match
var num_decors: int = decor_ids.size() * 2

# Define strict rotation angles (0, 90, 180, -90 degrees)
var allowed_rotations: Array[float] = [0.0, 90.0, 180.0, -90.0]

for i in range(num_decors):
var decor: Sprite2D = Sprite2D.new()
Expand All @@ -183,9 +193,18 @@ func setup_decor_layer(viewport: Vector2) -> void:
decor.texture = texture_preloader.get_resource(id)
decor.centered = false

var scale_factor: float = randf_range(0.5, 1.0)
# SCALING TRICK 1: Wider scale range (0.5 to 1.5) for more size variance
var scale_factor: float = randf_range(0.5, 1.5)
decor.scale = Vector2(scale_factor, scale_factor)

# SCALING TRICK 2: Randomly mirror the sprite horizontally and/or vertically
decor.flip_h = [true, false].pick_random()
decor.flip_v = [true, false].pick_random()
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

# Apply random cardinal rotation to ALL decor sprites
var random_degrees: float = allowed_rotations.pick_random()
decor.rotation = deg_to_rad(random_degrees)

decor.position.x = randf_range(0, viewport.x - (decor.texture.get_width() * scale_factor))
decor.position.y = randf_range(
0, layer_height - (decor.texture.get_height() * scale_factor)
Expand Down Expand Up @@ -248,10 +267,6 @@ func show_message(text: String, type: MessageType = MessageType.CRITICAL_UNBOUND
match type:
MessageType.KEY_PRESS_UNBOUND:
_showing_unbound_key_message = false
# CRITICAL_UNBOUND: Do NOT reset here (once-per-session intent)
# Reset only when bindings are fixed
# (e.g., in key_mapping.gd _on_conflict_confirmed or reset)
# _showing_unbound_warning = false # ← commented out


## Public: Clears the unbound warning flag after fixes.
Expand Down
119 changes: 119 additions & 0 deletions test/gut/test_decor_layer_transformations.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
## Copyright (C) 2026 Egor Kostan
## SPDX-License-Identifier: GPL-3.0-or-later
## test_decor_layer_transformations.gd
##
## GUT unit tests for verifying randomized rotations, scaling,
## and flipping applied to the main scene's decor layer.

extends "res://addons/gut/test.gd"

var main_scene: MainScene
var viewport_mock: Vector2 = Vector2(1920, 1080)


## Per-test setup: Isolate state and initialize scene.
## :rtype: void
func before_each() -> void:
await get_tree().process_frame
main_scene = preload("res://scenes/main_scene.tscn").instantiate()
add_child_autofree(main_scene)
await get_tree().process_frame


## Per-test teardown: Aggressive memory cleanup.
## :rtype: void
func after_each() -> void:
if is_instance_valid(main_scene):
main_scene.free()
await get_tree().process_frame


## test_decor_sprites_have_valid_cardinal_rotations |
## Verifies every decor sprite is snapped to exactly 0, 90, 180, or -90 degrees.
## :rtype: void
func test_decor_sprites_have_valid_cardinal_rotations() -> void:
gut.p("Testing: All decor sprites must use strict cardinal rotations.")

main_scene.setup_decor_layer(viewport_mock)

var active_sprites: Array[Node] = main_scene.decor_layer.get_children().filter(
func(c: Node) -> bool: return not c.is_queued_for_deletion()
)

var allowed_radians: Array[float] = [
0.0,
deg_to_rad(90.0),
deg_to_rad(180.0),
deg_to_rad(-90.0)
]

var invalid_rotations: Array[float] = []

for node in active_sprites:
var sprite := node as Sprite2D
var is_valid := false

# Check if the sprite's rotation matches any allowed radian (using approx to handle float drift)
for allowed_rad in allowed_radians:
if is_equal_approx(sprite.rotation, allowed_rad):
is_valid = true
break

if not is_valid:
invalid_rotations.append(sprite.rotation)

assert_eq(invalid_rotations.size(), 0, "Found decor sprites with non-cardinal rotations.")


## test_decor_sprites_have_valid_scale_ranges |
## Verifies every decor sprite scale falls between 0.5 and 1.5, and is uniformly scaled.
## :rtype: void
func test_decor_sprites_have_valid_scale_ranges() -> void:
gut.p("Testing: All decor sprites must be uniformly scaled between 0.5 and 1.5.")

main_scene.setup_decor_layer(viewport_mock)

var active_sprites: Array[Node] = main_scene.decor_layer.get_children().filter(
func(c: Node) -> bool: return not c.is_queued_for_deletion()
)

var out_of_bounds_scales: Array[Vector2] = []
var non_uniform_scales: Array[Vector2] = []

for node in active_sprites:
var sprite := node as Sprite2D
var s: Vector2 = sprite.scale

# Check bounds (using 0.49 and 1.51 to safely absorb floating point precision errors)
if s.x < 0.49 or s.x > 1.51:
out_of_bounds_scales.append(s)

# Check uniformity (x scale must equal y scale)
if not is_equal_approx(s.x, s.y):
non_uniform_scales.append(s)

assert_eq(out_of_bounds_scales.size(), 0, "Found decor sprites outside the 0.5 - 1.5 scale range.")
assert_eq(non_uniform_scales.size(), 0, "Found decor sprites with non-uniform (squished/stretched) scaling.")


## test_decor_sprites_have_boolean_flips |
## Verifies that flip_h and flip_v properties are actively being assigned.
## :rtype: void
func test_decor_sprites_have_boolean_flips() -> void:
gut.p("Testing: Decor sprites should successfully assign horizontal and vertical flips.")

main_scene.setup_decor_layer(viewport_mock)

var active_sprites: Array[Node] = main_scene.decor_layer.get_children().filter(
func(c: Node) -> bool: return not c.is_queued_for_deletion()
)

var null_flips_found: int = 0

for node in active_sprites:
var sprite := node as Sprite2D
# Verify the properties are valid booleans and not null/undefined
if typeof(sprite.flip_h) != TYPE_BOOL or typeof(sprite.flip_v) != TYPE_BOOL:
null_flips_found += 1

assert_eq(null_flips_found, 0, "All decor sprites must have valid boolean flip states.")
1 change: 1 addition & 0 deletions test/gut/test_decor_layer_transformations.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dn1cexpx24og0
Loading
Loading