Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 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
80 changes: 62 additions & 18 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,66 @@ func _ready() -> void:
# Setup decor layer with random instances
setup_decor_layer(viewport_size)

# =========================================================
# DEPENDENCY INJECTION: Parallax Background
# =========================================================
# Safely extract settings once to use for both injection and priming.
# The is_instance_valid(Globals) guard prevents hard crashes during
# isolated GUT tests where Autoloads may not be fully initialized.
var settings_res: GameSettingsResource = (
Globals.settings if is_instance_valid(Globals) else null
)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

if background.has_method("setup"):
background.setup(settings_res)
else:
push_warning(
"Parallax background is missing the `setup` method. Settings injection failed."
)

# Wire up the signal architecture for the parallax background
if player.has_signal("speed_changed") and background.has_method("_on_player_speed_changed"):
# 1. Guard against duplicate connections
if not player.speed_changed.is_connected(background._on_player_speed_changed):
player.speed_changed.connect(background._on_player_speed_changed)

# 2. Prime the background securely via a public method
if background.has_method("prime_speed"):
background.prime_speed(player.speed["speed"])
else:
push_warning("Parallax background is missing the `prime_speed` method.")

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)`."
)
)
# =========================================================
# FLOAT DEGRADATION SAFEGUARD
# =========================================================
# Calculate global wrap period based on the tallest layer to prevent float degradation.
# Formula: motion_mirroring.y / motion_scale.y
# Dynamically derived from the layer properties to prevent fragile magic numbers.
var bg_motion_scale: float = 0.5 # Safe fallback
if is_instance_valid(bushes_layer) and bushes_layer.motion_scale.y > 0.0:
bg_motion_scale = bushes_layer.motion_scale.y

var global_bushes_period: float = (viewport_size.y * parallax_screens_tall) / bg_motion_scale

if background.has_method("set_wrap_period"):
background.set_wrap_period(global_bushes_period)


# 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,23 +277,7 @@ func setup_decor_layer(viewport: Vector2) -> void:
decor_layer.motion_mirroring = Vector2(0, layer_height)


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 = (
Globals.settings if is_instance_valid(Globals) else null
)
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)

func _process(_delta: float) -> void:
# 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
94 changes: 94 additions & 0 deletions scripts/parallax_manager.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
## 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 via Dependency Injection and the Observer Pattern.

class_name ParallaxManager
extends ParallaxBackground

## Base multiplier applied to the final scroll math to scale the speed visually.
const SCROLL_MULTIPLIER: float = 0.8

## Optional wrap limit to prevent float32 precision degradation over long sessions.
## Should be a common multiple of the layers' (motion_mirroring.y / motion_scale.y).
@export var wrap_period: float = 0.0

var _current_speed: float = 0.0
var _difficulty: float = 1.0
var _out_of_fuel: bool = false


## Injects the game settings resource and wires up observer signals.
## Prevents tight coupling to global singletons in the process loop.
## @param settings: GameSettingsResource - The configuration resource.
## @return: void
func setup(settings: GameSettingsResource) -> void:
if not is_instance_valid(settings):
return

_difficulty = settings.difficulty
_out_of_fuel = (settings.current_fuel <= 0.0)

if not settings.setting_changed.is_connected(_on_setting_changed):
settings.setting_changed.connect(_on_setting_changed)
if not settings.fuel_depleted.is_connected(_on_fuel_depleted):
settings.fuel_depleted.connect(_on_fuel_depleted)


## Public method to prime the background's initial speed.
## Keeps private signal handlers properly encapsulated.
## @param initial_speed: float - The starting forward speed.
## @return: void
func prime_speed(initial_speed: float) -> void:
_current_speed = initial_speed


## Public method to dynamically set the wrap limit to prevent float precision loss.
## @param period: float - The calculated period to wrap the offset around.
## @return: void
func set_wrap_period(period: float) -> void:
wrap_period = period


## Observer callback triggered when the player's speed changes.
## @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


## Observer callback for specific setting updates (difficulty and refueling).
## @param setting_name: String - The name of the changed setting.
## @param new_value: Variant - The updated value.
## @return: void
func _on_setting_changed(setting_name: String, new_value: Variant) -> void:
if setting_name == "difficulty":
_difficulty = float(new_value)
elif setting_name == "current_fuel" and float(new_value) > 0.0:
_out_of_fuel = false


## Observer callback to instantly snap the background when fuel runs out.
## @return: void
func _on_fuel_depleted() -> void:
_out_of_fuel = true
scroll_offset = Vector2.ZERO


## Called every physics/rendering frame.
## Updates scroll offset based entirely on cached local variables
## and wraps to preserve float precision.
## @param delta: float - The elapsed time since the previous frame.
## @return: void
func _process(delta: float) -> void:
if _out_of_fuel:
scroll_offset = Vector2.ZERO
else:
var scroll_amount: float = _current_speed * delta * _difficulty * SCROLL_MULTIPLIER
scroll_offset.y += scroll_amount

# Prevent float precision degradation by wrapping modulo the period
if wrap_period > 0.0:
scroll_offset.y = wrapf(scroll_offset.y, 0.0, wrap_period)
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
Loading
Loading