Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6c8427d
[FEATURE] Implement Parallax Scrolling Background linked to Player Sp…
ikostan Apr 17, 2026
1246521
Update main_scene.gd
ikostan Apr 17, 2026
b137b63
Update scripts/main_scene.gd
ikostan Apr 17, 2026
e436db6
Update main_scene.gd
ikostan Apr 17, 2026
e3a9b70
Update main_scene.gd
ikostan Apr 17, 2026
7040237
Here is a comprehensive GUT unit test suite for the new parallax_mana…
ikostan Apr 17, 2026
d6b27ed
Update parallax_manager.gd
ikostan Apr 18, 2026
64b1bbc
completely eliminate the _process polling by using Dependency Inject…
ikostan Apr 18, 2026
1dbb09d
Missing coverage for the fuel-depletion reset, and tests implicitly d…
ikostan Apr 18, 2026
6af4227
Update test/gut/test_parallax_manager.gd
ikostan Apr 18, 2026
d5cc59a
Update main_scene.gd
ikostan Apr 18, 2026
39bd963
Update parallax_manager.gd
ikostan Apr 18, 2026
ee92636
Update main_scene.gd
ikostan Apr 18, 2026
43ed4c2
Update main_scene.gd
ikostan Apr 18, 2026
f6eeb1f
Update main_scene.gd
ikostan Apr 18, 2026
19467a2
scroll_offset.y grows unboundedly — consider wrapping to preserve flo…
ikostan Apr 19, 2026
bc0702e
Update main_scene.gd
ikostan Apr 19, 2026
b101d71
Update parallax_manager.gd
ikostan Apr 19, 2026
9abb76a
126-148: Nit: test name/assertion message is slightly misleading.
ikostan Apr 19, 2026
2c99f7c
63-67: LGTM — refuel recovery path is correctly wired.
ikostan Apr 19, 2026
27a0903
Update main_scene.gd
ikostan Apr 19, 2026
0117f9f
Update parallax_manager.gd
ikostan Apr 19, 2026
87efef1
Update main_scene.gd
ikostan Apr 19, 2026
189ec21
Update main_scene.gd
ikostan Apr 19, 2026
14084a0
This fully resolves the reviewer's concern by creating a proper, publ…
ikostan Apr 19, 2026
b7d5b25
let the ParallaxManager inspect its own children and calculate the ma…
ikostan Apr 19, 2026
ba0fb22
player's speed is now rigorously typed
ikostan Apr 19, 2026
cafaa32
Unit test updates
ikostan Apr 19, 2026
a8a50fe
Update test/gut/test_parallax_manager.gd
ikostan Apr 19, 2026
539986c
Update GUT unit tests
ikostan Apr 19, 2026
e1c8628
Merge branch 'implement-parallax-scrolling-background-linked-to-playe…
ikostan Apr 19, 2026
de072dc
auto_calculate_wrap_period() silently disables wrap when no layers ar…
ikostan Apr 19, 2026
d08fb80
Update parallax_manager.gd
ikostan Apr 19, 2026
cac27df
Optional: max-of-periods is only safe when layer periods are commens…
ikostan Apr 19, 2026
5fdc662
Update scripts/main_scene.gd
ikostan Apr 19, 2026
4c3c8ec
Update main_scene.gd
ikostan Apr 19, 2026
e6cfa37
Update parallax_manager.gd
ikostan Apr 19, 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
4 changes: 3 additions & 1 deletion scenes/main_scene.tscn
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[gd_scene load_steps=82 format=3 uid="uid://nnnc0qhx07i8"]
[gd_scene load_steps=83 format=3 uid="uid://nnnc0qhx07i8"]

[ext_resource type="Script" uid="uid://ctm7qg12s2swt" path="res://scripts/main_scene.gd" id="1_7ykc4"]
[ext_resource type="PackedScene" uid="uid://cb4n4cqkuddqg" path="res://scenes/pause_menu.tscn" id="1_w2twt"]
Expand Down Expand Up @@ -77,6 +77,7 @@
[ext_resource type="Texture2D" uid="uid://bvxu5x1awjrjv" path="res://files/random_decor/dirt_001.png" id="69_7tyuc"]
[ext_resource type="Texture2D" uid="uid://f4hxu68qa4fi" path="res://files/random_decor/dirt_002.png" id="70_isor2"]
[ext_resource type="Script" uid="uid://blu5qujicfa7e" path="res://scripts/hud.gd" id="72_sgkfd"]
[ext_resource type="Script" uid="uid://b5x2ehdthhrla" path="res://scripts/parallax_manager.gd" id="76_qj6t7"]

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pu3yx"]
bg_color = Color(0.2627451, 0.2627451, 0.2627451, 0.5882353)
Expand Down Expand Up @@ -204,6 +205,7 @@ step = 1.0
[node name="PauseMenu" parent="." instance=ExtResource("1_w2twt")]

[node name="Background" type="ParallaxBackground" parent="."]
script = ExtResource("76_qj6t7")

[node name="Sand" type="ParallaxLayer" parent="Background"]

Expand Down
34 changes: 24 additions & 10 deletions scripts/main_scene.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Copyright (C) 2025 Egor Kostan
## Copyright (C) 2026 Egor Kostan
## SPDX-License-Identifier: GPL-3.0-or-later
## main_scene.gd
## Main scene script for SkyLockAssault.
Expand Down Expand Up @@ -52,6 +52,28 @@ func _ready() -> void:
# Setup decor layer with random instances
setup_decor_layer(viewport_size)

# Wire up the signal architecture for the parallax background
if player.has_signal("speed_changed") and background.has_method("_on_player_speed_changed"):
player.speed_changed.connect(background._on_player_speed_changed)
# Prime the background with the player's initial starting speed
background._on_player_speed_changed(player.speed["speed"], 0.0)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
elif not player.has_signal("speed_changed"):
push_warning(
(
"Parallax background not wired: player is missing the `speed_changed` signal. "
+ "Verify that the Player node defines and emits `speed_changed`."
)
)
elif not background.has_method("_on_player_speed_changed"):
push_warning(
(
"Parallax background not wired: background is missing"
+ " `_on_player_speed_changed` method. "
+ "Ensure the background script implements "
+ " `_on_player_speed_changed(speed: float, delta: float)`."
)
)


# 2. Detect when player presses a key/button that has NO binding at all
# Only significant inputs (axes above deadzone) are checked to prevent
Expand Down Expand Up @@ -217,7 +239,7 @@ func setup_decor_layer(viewport: Vector2) -> void:
decor_layer.motion_mirroring = Vector2(0, layer_height)


func _process(delta: float) -> void:
func _process(_delta: float) -> void:
# NEW: Safely grab the settings resource and guard against null crashes
# during scene transitions, engine shutdown, or isolated GUT tests.
var settings_res: GameSettingsResource = (
Expand All @@ -226,14 +248,6 @@ func _process(delta: float) -> void:
if not is_instance_valid(settings_res):
return

# Use the safe local reference for difficulty
var scroll_speed: float = player.speed["speed"] * delta * settings_res.difficulty * 0.8
background.scroll_offset.y += scroll_speed

# Use the safe local reference for current_fuel
if settings_res.current_fuel <= 0:
background.scroll_offset = Vector2(0, 0)

# 1. Critical unbound controls warning (shown ONCE per session)
# Flag stays true until player fixes bindings (e.g., in key_mapping.gd after remap).
# Do NOT reset here — that would make it repeat every 4s (bug fixed).
Expand Down
40 changes: 40 additions & 0 deletions scripts/parallax_manager.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Copyright (C) 2026 Egor Kostan
## SPDX-License-Identifier: GPL-3.0-or-later
## parallax_manager.gd
## Manages the scrolling speed of the parallax background based on player velocity.
## Decoupled from direct physics polling via the Observer Pattern.

class_name ParallaxManager
extends ParallaxBackground

var _current_speed: float = 0.0


## Observer callback triggered when the player's speed changes.
## Updates the internal speed used for parallax scrolling.
## @param new_speed: float - The new forward speed of the player.
## @param _max_speed: float - The maximum speed threshold (unused).
## @return: void
func _on_player_speed_changed(new_speed: float, _max_speed: float) -> void:
_current_speed = new_speed


## Called every physics/rendering frame. Updates the vertical scroll offset
## based on the cached player speed and current game difficulty.
## Explicitly resets the scroll offset to zero if the player runs out of fuel.
## @param delta: float - The elapsed time since the previous frame.
## @return: void
func _process(delta: float) -> void:
var difficulty: float = 1.0
var current_fuel: float = 1.0 # Default to > 0 to prevent accidental resets if Globals is null

if is_instance_valid(Globals) and is_instance_valid(Globals.settings):
difficulty = Globals.settings.difficulty
current_fuel = Globals.settings.current_fuel

# Enforce the legacy behavior: reset offset immediately on flameout
if current_fuel <= 0.0:
scroll_offset = Vector2.ZERO
else:
var scroll_amount: float = _current_speed * delta * difficulty * 0.8
scroll_offset.y += scroll_amount
1 change: 1 addition & 0 deletions scripts/parallax_manager.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b5x2ehdthhrla
128 changes: 128 additions & 0 deletions test/gut/test_parallax_manager.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
## Copyright (C) 2026 Egor Kostan
## SPDX-License-Identifier: GPL-3.0-or-later
## test_parallax_manager.gd
##
## GUT unit tests for the ParallaxManager script.
## Validates observer pattern synchronization, scroll offset math, and fallback safety.
extends "res://addons/gut/test.gd"

var _parallax_manager: ParallaxManager
var _original_settings: GameSettingsResource


## Per-test setup: Isolates the global resource state and instantiates the manager.
## :rtype: void
func before_each() -> void:
_original_settings = Globals.settings
Globals.settings = GameSettingsResource.new()
Globals.settings.difficulty = 1.0

_parallax_manager = ParallaxManager.new()
add_child_autofree(_parallax_manager)
Comment thread
ikostan marked this conversation as resolved.


## Post-test cleanup: Restores global state to prevent test leakage.
## :rtype: void
func after_each() -> void:
Globals.settings = _original_settings


# ==========================================
# OBSERVER INTEGRATION TESTS
# ==========================================

## test_speed_update_from_signal | Observer Integration
## :rtype: void
func test_speed_update_from_signal() -> void:
gut.p("Testing: ParallaxManager correctly caches speed from the player's signal.")

# Simulate the Player broadcasting a new speed of 250.0
_parallax_manager._on_player_speed_changed(250.0, 500.0)

assert_eq(
_parallax_manager._current_speed,
250.0,
"Manager must update _current_speed when signal callback is invoked."
)


# ==========================================
# PROCESS & MATH LOGIC TESTS
# ==========================================

## test_scroll_offset_math | Process Logic
## :rtype: void
func test_scroll_offset_math() -> void:
gut.p("Testing: Process loop correctly calculates the scroll offset increment based on difficulty.")

# 1. Setup specific variables for predictable math
Globals.settings.difficulty = 2.0
_parallax_manager._current_speed = 100.0
_parallax_manager.scroll_offset.y = 0.0

# 2. Simulate one physics frame
var delta: float = 0.5
_parallax_manager._process(delta)

# 3. Verify the math
# Expected math: speed(100.0) * delta(0.5) * diff(2.0) * multiplier(0.8) = 80.0
var expected_offset: float = 100.0 * 0.5 * 2.0 * 0.8

assert_almost_eq(
_parallax_manager.scroll_offset.y,
expected_offset,
0.01,
"Scroll offset must accurately reflect the delta, speed, and difficulty multiplier."
)


## test_zero_speed_stops_scroll | State Management
## :rtype: void
func test_zero_speed_stops_scroll() -> void:
gut.p("Testing: A speed of 0.0 results in a halted background scroll.")

# 1. Setup flameout/halt state
_parallax_manager._current_speed = 0.0
var initial_offset: float = 125.5
_parallax_manager.scroll_offset.y = initial_offset

# 2. Simulate processing frame
_parallax_manager._process(1.0)

# 3. Verify no movement
assert_eq(
_parallax_manager.scroll_offset.y,
initial_offset,
"Scroll offset must remain completely unchanged when speed is zero."
)


# ==========================================
# SAFETY & EDGE CASE TESTS
# ==========================================

## test_process_safe_without_globals | Safety Constraint
## :rtype: void
func test_process_safe_without_globals() -> void:
gut.p("Testing: ParallaxManager defaults to difficulty 1.0 if Globals.settings is missing.")

# 1. Force a null state (simulating scene transition or engine shutdown)
Globals.settings = null

_parallax_manager._current_speed = 100.0
_parallax_manager.scroll_offset.y = 0.0

# 2. Simulate processing frame
var delta: float = 1.0
_parallax_manager._process(delta)

# 3. Verify the math defaulted safely
# Expected math: speed(100.0) * delta(1.0) * diff(1.0 fallback) * multiplier(0.8) = 80.0
var expected_offset: float = 100.0 * 1.0 * 1.0 * 0.8

assert_almost_eq(
_parallax_manager.scroll_offset.y,
expected_offset,
0.01,
"Process must fall back to a 1.0 difficulty multiplier without throwing null instance errors."
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
1 change: 1 addition & 0 deletions test/gut/test_parallax_manager.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://8158s2i62s66
Loading