Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
cc33c56
Emit speed_changed signal; add tests
ikostan Apr 11, 2026
bd9f03c
Update player.gd
ikostan Apr 11, 2026
1590492
Test name and assertions are inconsistent (max_and_min but only max i…
ikostan Apr 12, 2026
d857fe3
Update player.gd
ikostan Apr 12, 2026
e0715d7
Update player.gd
ikostan Apr 12, 2026
aa9fdc8
Update test_player_movement_signals.gd
ikostan Apr 12, 2026
a861547
Add HUD, wire it up & refactor player/settings
ikostan Apr 12, 2026
f7d0a6c
Update game_settings_resource.gd
ikostan Apr 12, 2026
56a18a8
Update main_scene.gd
ikostan Apr 12, 2026
3237fd1
Refactor tests to use Globals settings & HUD
ikostan Apr 14, 2026
ce6864d
Update tests for Globals/HUD refactor
ikostan Apr 14, 2026
7b8127c
Update test_helpers.gd
ikostan Apr 14, 2026
5459961
Update player.gd
ikostan Apr 14, 2026
625942a
Update test_player_movement_signals.gd
ikostan Apr 14, 2026
e765c1f
Extract GUT mock builder to helper
ikostan Apr 14, 2026
bdd2377
Update scripts/game_settings_resource.gd
ikostan Apr 14, 2026
9944fd7
Update game_settings_resource.gd
ikostan Apr 14, 2026
4553ccd
Update game_settings_resource.gd
ikostan Apr 14, 2026
a358b22
Update scripts/hud.gd
ikostan Apr 14, 2026
4d2c69d
Use GameSettingsResource for the speed thresholds instead of duplicat…
ikostan Apr 14, 2026
c957693
Update hud.gd
ikostan Apr 14, 2026
afdb474
Fix HUD logging and connection guards; add tests
ikostan Apr 14, 2026
2122a7a
Update globals.gd
ikostan Apr 14, 2026
6195b8c
Update hud.gd
ikostan Apr 14, 2026
7557229
Update test_player.gd
ikostan Apr 14, 2026
5d8f016
Update scripts/game_settings_resource.gd
ikostan Apr 14, 2026
4e289dc
Update game_settings_resource.gd
ikostan Apr 14, 2026
0746137
Update hud.gd
ikostan Apr 14, 2026
6cc9a94
issue (bug_risk): high_yellow_fraction and low_yellow_fraction can be…
ikostan Apr 14, 2026
3ee148e
issue (bug_risk): setup_hud assumes the player_node has a speed_chang…
ikostan Apr 14, 2026
910d718
Release simulated actions before erasing them.
ikostan Apr 14, 2026
6d0f630
This flameout assertion is currently ambiguous.
ikostan Apr 14, 2026
efecc65
suggestion (bug_risk): HUD does not react to speed-related setting ch…
ikostan Apr 14, 2026
c69e1fc
Expose HUD accessors and update tests
ikostan Apr 14, 2026
d6f09f5
Update hud.gd
ikostan Apr 14, 2026
27dff10
Update hud.gd
ikostan Apr 14, 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
25 changes: 25 additions & 0 deletions scripts/player.gd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ extends Node2D
## Player controller for P-38 Lightning in SkyLockAssault.
## Manages movement, fuel, bounds, rotors (anim/sound), weapons.

## Emitted when the player's forward speed changes.
signal speed_changed(new_speed: float)

# Bounds hitbox scale (quarter texture = tight margin for top-down plane)
const HITBOX_SCALE: float = 0.25

Expand Down Expand Up @@ -213,6 +216,9 @@ func _ready() -> void:
speed["timer"].wait_time = BLINK_INTERVAL
speed["timer"].one_shot = false # Repeat indefinitely
speed["timer"].timeout.connect(_on_speed_blink_timer_timeout)

# Connect speed signal
speed_changed.connect(_on_speed_changed)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Init speed bar
speed["bar"].max_value = speed["max"] # Set max speed value
Expand All @@ -229,6 +235,11 @@ func _ready() -> void:
push_error("Weapon node not found! Check player.tscn scene tree for $Weapon child.")


func _on_speed_changed(_new_speed: float) -> void:
update_speed_bar()
check_speed_warning()


# NEW: Defensive cleanup to prevent dangling signal connections
# when the player is removed from the scene tree or reloaded.
func _exit_tree() -> void:
Expand All @@ -238,6 +249,9 @@ func _exit_tree() -> void:

if _settings.fuel_depleted.is_connected(_on_player_out_of_fuel):
_settings.fuel_depleted.disconnect(_on_player_out_of_fuel)

if speed_changed.is_connected(_on_speed_changed):
speed_changed.disconnect(_on_speed_changed)


# NEW: Observer pattern handler to react when GameSettingsResource
Expand Down Expand Up @@ -280,7 +294,11 @@ func _on_player_out_of_fuel() -> void:
Globals.log_message("Player is out of fuel! Engine flameout.", Globals.LogLevel.WARNING)

# NEW: Migrated the speed reset to ensure the plane actually stops flying when fuel hits 0
var old_speed: float = speed["speed"]
speed["speed"] = 0.0

if old_speed != speed["speed"]:
speed_changed.emit(speed["speed"])

rotor_stop(rotor_right, rotor_right_sfx)
rotor_stop(rotor_left, rotor_left_sfx)
Expand Down Expand Up @@ -529,6 +547,9 @@ func _physics_process(_delta: float) -> void:
# NEW: Guard against null references during teardown or tests
if not is_instance_valid(_settings):
return

# Track speed to emit signal on change
var old_speed: float = speed["speed"]

# Speed changes allowed only if fuel > 0
if Input.is_action_pressed("speed_up") and _settings.current_fuel > 0:
Expand All @@ -543,6 +564,10 @@ func _physics_process(_delta: float) -> void:
speed["speed"] = clamp(speed["speed"], 0, speed["max"])
else:
speed["speed"] = clamp(speed["speed"], speed["min"], speed["max"])

# Emit signal if speed actually changed
if old_speed != speed["speed"]:
speed_changed.emit(speed["speed"])
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Left/Right movement
var lateral_input: float = Input.get_axis("move_left", "move_right")
Expand Down
207 changes: 207 additions & 0 deletions test/gut/test_player_fuel_logic.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
## Copyright (C) 2026 Egor Kostan
## SPDX-License-Identifier: GPL-3.0-or-later
## test_player_fuel_logic.gd
## GUT unit tests for Player fuel consumption, engine states, and UI Reactivity.
extends "res://addons/gut/test.gd"

const PLAYER_SCRIPT_PATH: String = "res://scripts/player.gd"

var _mock_root: Node
var _player: Variant # CHANGED: Use Variant to allow dynamic property access to player.gd variables
var _original_settings: GameSettingsResource
var _added_actions: Array[String] = []

## Per-test setup.
## :rtype: void
func before_each() -> void:
_original_settings = Globals.settings
Globals.settings = GameSettingsResource.new()
Globals.settings.current_log_level = Globals.LogLevel.NONE

for action: String in ["speed_up", "speed_down", "move_left", "move_right"]:
if not InputMap.has_action(action):
InputMap.add_action(action)
_added_actions.append(action)

_mock_root = _build_mock_player_scene()
add_child_autoqfree(_mock_root)
_player = _mock_root.get_node("Player")

## Per-test cleanup.
## :rtype: void
func after_each() -> void:
Globals.settings = _original_settings
for action: String in _added_actions:
InputMap.erase_action(action)
_added_actions.clear()
Input.action_release("move_left")

## test_ui_updates_automatically_on_resource_change | Observer Pattern
## :rtype: void
func test_ui_updates_automatically_on_resource_change() -> void:
gut.p("Testing: Player UI responds seamlessly to external fuel updates.")

var fuel_bar: ProgressBar = _player.fuel_bar

Globals.settings.max_fuel = 200.0
# Because of the resource setter, current_fuel modification fires 'setting_changed' automatically
Globals.settings.current_fuel = 150.0

assert_eq(fuel_bar.max_value, 200.0, "Fuel Bar max_value must sync with Resource max.")
assert_eq(fuel_bar.value, 150.0, "Fuel Bar value must sync automatically.")

## test_engine_stops_on_zero_fuel | Component State
## :rtype: void
func test_engine_stops_on_zero_fuel() -> void:
gut.p("Testing: Zero fuel stops timers and rotor animations immediately.")

_player.fuel_timer.start()
var anim_r: AnimatedSprite2D = _player.rotor_right.get_node("AnimatedSprite2D")
anim_r.play("default")

_player._on_player_out_of_fuel()

assert_true(_player.fuel_timer.is_stopped(), "Fuel timer must stop running on flameout.")
assert_false(anim_r.is_playing(), "Rotors must stop animating when fuel is empty.")

## test_engine_reignites_on_refuel | Component State
## :rtype: void
func test_engine_reignites_on_refuel() -> void:
gut.p("Testing: Refueling from an empty tank restarts rotors and timers.")

# Simulate dead engine
_player.fuel_timer.stop()
var anim_l: AnimatedSprite2D = _player.rotor_left.get_node("AnimatedSprite2D")
anim_l.stop()

# Trigger the global setting change to simulate refuel logic
Globals.settings.current_fuel = 50.0

assert_false(_player.fuel_timer.is_stopped(), "Fuel timer must reignite on refuel.")
assert_true(anim_l.is_playing(), "Rotors must automatically resume spinning.")

## test_lateral_movement_blocked_without_fuel | Movement Constraints
## :rtype: void
func test_lateral_movement_blocked_without_fuel() -> void:
gut.p("Testing: Lateral turning is disabled if fuel is completely empty.")

Globals.settings.current_fuel = 0.0
_player.speed["speed"] = 150.0
_player.player.velocity.x = 0.0

Input.action_press("move_left")
_player._physics_process(0.1)

assert_eq(float(_player.player.velocity.x), 0.0, "Plane must not turn without fuel, ignoring inputs.")

# ==========================================
# MOCK BUILDER HELPER
# ==========================================
# Note: You can optionally extract this into a shared res://tests/test_helpers.gd base class later!
## Dynamically constructs the node hierarchy required by player.gd.
## :rtype: Node
func _build_mock_player_scene() -> Node:
var root: Node = Node.new()
root.name = "MockLevel"

var panel: Panel = Panel.new()
panel.name = "PlayerStatsPanel"
var stats: Control = Control.new()
stats.name = "Stats"

var fuel: Control = Control.new()
fuel.name = "Fuel"
var fuel_bar: ProgressBar = ProgressBar.new()
fuel_bar.name = "FuelBar"
var fuel_label: Label = Label.new()
fuel_label.name = "FuelLabel"
var f_timer: Timer = Timer.new()
f_timer.name = "BlinkTimer"
fuel_label.add_child(f_timer)
fuel.add_child(fuel_bar)
fuel.add_child(fuel_label)

var speed: Control = Control.new()
speed.name = "Speed"
var speed_bar: ProgressBar = ProgressBar.new()
speed_bar.name = "SpeedBar"
var speed_label: Label = Label.new()
speed_label.name = "SpeedLabel"
var s_timer: Timer = Timer.new()
s_timer.name = "BlinkTimer"
speed_label.add_child(s_timer)
speed.add_child(speed_bar)
speed.add_child(speed_label)

stats.add_child(fuel)
stats.add_child(speed)
panel.add_child(stats)
root.add_child(panel)

var PlayerScript := load(PLAYER_SCRIPT_PATH)
var p_node: Variant = PlayerScript.new()
p_node.name = "Player"

var cb2d: CharacterBody2D = CharacterBody2D.new()
cb2d.name = "CharacterBody2D"

for rotor_name: String in ["RotorRight", "RotorLeft"]:
var rotor: Node2D = Node2D.new()
rotor.name = rotor_name
var sfx: AudioStreamPlayer2D = AudioStreamPlayer2D.new()
sfx.name = "AudioStreamPlayer2D"
var anim: AnimatedSprite2D = AnimatedSprite2D.new()
anim.name = "AnimatedSprite2D"
# var frames: SpriteFrames = SpriteFrames.new()
# frames.add_animation("default")
# anim.sprite_frames = frames

var frames: SpriteFrames = SpriteFrames.new()
frames.add_animation("default")
# Add a dummy frame so play() actually engages and is_playing() returns true
var dummy_tex: PlaceholderTexture2D = PlaceholderTexture2D.new()
frames.add_frame("default", dummy_tex)
anim.sprite_frames = frames

rotor.add_child(anim)
rotor.add_child(sfx)
cb2d.add_child(rotor)

var sprite: Sprite2D = Sprite2D.new()
sprite.name = "Sprite2D"
var coll: CollisionPolygon2D = CollisionPolygon2D.new()
coll.name = "CollisionPolygon2D"
#var weapon: Node2D = Node2D.new()
#weapon.name = "Weapon"

var weapon: Node2D = Node2D.new()
weapon.name = "Weapon"

# Create a dummy script so player.gd's _ready() and _input() don't crash
var mock_weapon_script: GDScript = GDScript.new()
mock_weapon_script.source_code = """
extends Node2D
var weapon_types: Array = []
var current_index: int = 0
func fire() -> void:
pass
func get_num_weapons() -> int:
return 1
func switch_to(idx: int) -> void:
pass
"""
mock_weapon_script.reload()
weapon.set_script(mock_weapon_script)

cb2d.add_child(sprite)
cb2d.add_child(coll)
cb2d.add_child(weapon)

var fuel_timer: Timer = Timer.new()
fuel_timer.name = "FuelTimer"

p_node.add_child(cb2d)
p_node.add_child(fuel_timer)
root.add_child(p_node)

return root
1 change: 1 addition & 0 deletions test/gut/test_player_fuel_logic.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b0q7q3g1c058o
Loading
Loading