Skip to content
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
e2b619c
Merge pull request #573 from ikostan/main
ikostan Apr 17, 2026
6e01bc9
Create slider.wav
espanakosta-jpg Apr 19, 2026
9d4036a
Create slider.wav.import
espanakosta-jpg Apr 19, 2026
cacff88
Merge pull request #577 from ikostan/main
ikostan Apr 21, 2026
0a50250
Merge pull request #586 from ikostan/main
ikostan Apr 23, 2026
6f83b6f
[DOCUMENTATION] Milestone #16 README.md update #456
ikostan Apr 23, 2026
84b3cff
Update README.md
ikostan Apr 23, 2026
5ddea6d
https://github.com/ikostan/SkyLockAssault/issues/565
ikostan Apr 23, 2026
77c27ad
[FEATURE] Connect VolumeSlider to Contextual Audio Feedback #566
ikostan Apr 23, 2026
c415e22
Update globals.gd
ikostan Apr 24, 2026
461b09d
Update volume_slider.gd
ikostan Apr 24, 2026
d747662
Update .all-contributorsrc
ikostan Apr 24, 2026
d668e65
suggestion (bug_risk): Programmatic updates while the slider has focu…
ikostan Apr 24, 2026
41a65f5
Update README.md
ikostan Apr 24, 2026
95acc70
Update .all-contributorsrc
ikostan Apr 24, 2026
4e2fda8
Merge branch 'SFX' of https://github.com/ikostan/SkyLockAssault into SFX
ikostan Apr 24, 2026
3f5112f
Update volume_slider.gd
ikostan Apr 24, 2026
ba62539
Update volume_slider.gd
ikostan Apr 24, 2026
0741179
audio_constants.gd singleton specifically built for preventing typos …
ikostan Apr 24, 2026
2e5bca6
Update volume_slider.gd
ikostan Apr 24, 2026
63e6a2a
Update README.md
ikostan Apr 24, 2026
28d30a3
Update scripts/managers/audio_manager.gd
ikostan Apr 24, 2026
4d17a2f
Update audio_manager.gd
ikostan Apr 24, 2026
9261c1a
[FEATURE] Verify Signal Decoupling for Web and UI Sync #567
ikostan Apr 24, 2026
b71e4a9
New GUT unit tests
ikostan Apr 24, 2026
11f4eba
Update volume_slider.gd
ikostan Apr 24, 2026
dc1ebea
Update volume_slider.gd
ikostan Apr 24, 2026
ec7ecb0
Update audio_manager.gd
ikostan Apr 24, 2026
14a6f1d
Update audio_manager.gd
ikostan Apr 24, 2026
1caf738
New GUT tests
ikostan Apr 24, 2026
4eb74e0
Update test_volume_slider.gd
ikostan Apr 24, 2026
2c92711
issue (bug_risk): Guard against invalid audio bus names to avoid runt…
ikostan Apr 25, 2026
fc74ecc
Update scripts/ui/components/volume_slider.gd
ikostan Apr 25, 2026
0b6700a
Update scripts/ui/components/volume_slider.gd
ikostan Apr 25, 2026
951c849
Test updates
ikostan Apr 25, 2026
07d4012
Update scripts/ui/components/volume_slider.gd
ikostan Apr 25, 2026
df74365
Drag state may get stuck if release event is missed.
ikostan Apr 25, 2026
47596d6
suggestion (performance): Avoid repeated warnings and load attempts f…
ikostan Apr 26, 2026
71997b5
Use one English variant consistently (centralised vs centralized).
ikostan Apr 26, 2026
b1872eb
Restore AudioManager state after each test to prevent cross-suite lea…
ikostan Apr 26, 2026
566f5bc
Rate-limit test can be flaky under load.
ikostan Apr 26, 2026
2db4d9b
Test mutates live AudioManager / audio server state without isolation.
ikostan Apr 26, 2026
a760da5
test_sfx_guard_allows_valid_interaction will trigger real audio playb…
ikostan Apr 26, 2026
9b50fbb
Duplicate section header.
ikostan Apr 26, 2026
b8c448f
Connect finished before calling play() for robustness.
ikostan Apr 26, 2026
e14872c
Update audio_manager.gd
ikostan Apr 26, 2026
a001b09
Update audio_manager.gd
ikostan Apr 26, 2026
becbbbb
Encapsulating VolumeSlider
ikostan Apr 26, 2026
7c3afcd
Update volume_slider.gd
ikostan Apr 26, 2026
d545aa1
Guard ordering: _previous_value is committed before interaction/rate …
ikostan Apr 26, 2026
ddf4015
Debounce timer restarts even when the new value is effectively identi…
ikostan Apr 26, 2026
c179b1d
Update test_volume_slider.gd
ikostan Apr 26, 2026
12c21c3
Update volume_slider.gd
ikostan Apr 26, 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
1 change: 1 addition & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"files": ["README.md"],
"imageSize": 100,
"contributorsPerLine": 7,
"contributors": [],
"badgeTemplate": "[<image-card alt=\"All Contributors\" src=\"https://img.shields.io/github/all-contributors/<%= projectOwner %>/<%= projectName %>?color=ee8449&style=flat-square\" ></image-card>](<%= contributorsUrl %>)"
}
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ You can play this game on [Itch.io](https://ikostan.itch.io/sky-lock-assault)
- [Release Drafter](https://github.com/release-drafter/release-drafter?tab=readme-ov-file#readme)
- [Close Stale Issues and PRs](https://github.com/actions/stale)
- [AllContributors GitHub App](https://allcontributors.org/docs/en/bot/installation)
- [DeepSource](https://github.com/deepsource)
9. [Free Web Browser Game Deployment Platforms](files/docs/Platforms_for_Web_Deployment_Guide.md)
<!-- markdownlint-enable line-length -->

Expand Down Expand Up @@ -145,6 +146,21 @@ these GPL requirements, a separate license is available upon request.
- Observer-based Settings System: Centralized GameSettingsResource that handles
automatic persistence and UI synchronization through signals.


### Project Structure (`scripts/`)

Post-Refactor Phase 4 (PR `#582`), the root `scripts/` directory has been fully
reorganised into purpose-specific sub-directories:
<!-- markdownlint-disable MD013 -->
| Directory | Contents |
|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `scripts/core/` | Foundational systems: `game_paths.gd` (centralized path registry), `globals.gd`, `main_scene.gd`, `settings.gd` |
| `scripts/resources/` | Data containers & configuration: `game_settings_resource.gd`, `audio_constants.gd` |
| `scripts/entities/` | Game objects: `player.gd`, `bullet.gd`, `weapon.gd` |
| `scripts/system/` | Platform wrappers & integrations: `audio_web_bridge.gd`, `JavaScriptBridgeWrapper.gd`, `OSWrapper.gd` |
| `scripts/managers/` | Game-loop managers: `audio_manager.gd`, `parallax_manager.gd`, `resource_preloader.gd` |
| `scripts/ui/` | Interface layer: `hud.gd`; sub-dirs `menus/` (main, pause, options, audio, gameplay, key-mapping, advanced), `screens/` (splash, loading), `components/` (volume slider, input remap button) |
<!-- markdownlint-enable MD013 -->
---

## 🟢 Current Development Status
Expand Down Expand Up @@ -289,6 +305,19 @@ to user input devices:
- Modifier-aware remapping requires explicit key+modifier press for
unique bindings.

### Milestone 14

**Status:** Stable gameplay loop with synced UI systems and GUT-based
unit testing.
**Active Focus:** Gameplay expansion (AI enemies, multiplayer, levels).
**Version:** v0.9.18

### Milestone 16

**Status:** Stable gameplay loop with fully refactored script architecture,
synced UI systems, and GUT-based unit testing.
**Active Focus:** Gameplay expansion (AI enemies, multiplayer, levels).

Track progress via [Milestones](https://github.com/ikostan/SkyLockAssault/milestones).

---
Expand Down
Binary file added files/sounds/sfx/slider.wav
Binary file not shown.
24 changes: 24 additions & 0 deletions files/sounds/sfx/slider.wav.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[remap]

importer="wav"
type="AudioStreamWAV"
uid="uid://6asmf6p6ftp5"
path="res://.godot/imported/slider.wav-562467666e90bfee2364ab6985972073.sample"

[deps]

source_file="res://files/sounds/sfx/slider.wav"
dest_files=["res://.godot/imported/slider.wav-562467666e90bfee2364ab6985972073.sample"]

[params]

force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Comment thread
coderabbitai[bot] marked this conversation as resolved.
8 changes: 7 additions & 1 deletion scripts/core/globals.gd
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,8 @@ static func set_game_version_for_tests(value: String) -> void:
## Use _input instead of _unhandled_input to catch events BEFORE the UI consumes them.
func _input(_event: InputEvent) -> void:
# The Ultimate Menu Check: Does a UI element currently have keyboard/gamepad focus?
var ui_has_focus: bool = is_instance_valid(get_viewport().gui_get_focus_owner())
var focus_owner: Control = get_viewport().gui_get_focus_owner()
var ui_has_focus: bool = is_instance_valid(focus_owner)

# Gate 1: Only play UI sounds if a UI element is focused OR we are in a known menu state
var is_menu_context: bool = (
Expand All @@ -392,6 +393,11 @@ func _input(_event: InputEvent) -> void:
# We use the global Input singleton here because it perfectly handles
# analog joystick deadzone debouncing, which event.is_echo() misses.
if Input.is_action_just_pressed(action):
# NEW: Prevent double-audio when adjusting sliders.
# If a slider has focus, left/right adjusts the value instead of navigating.
if focus_owner is Slider and (action == "ui_left" or action == "ui_right"):
return

_play_ui_navigation_sfx()
return # Exit once sound is triggered to avoid double-plays

Expand Down
105 changes: 104 additions & 1 deletion scripts/managers/audio_manager.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ signal volume_changed(bus_name: String, volume: float)
signal mute_toggled(bus_name: String, is_muted: bool)
# --------------------------------------------

# --- NEW: SFX CACHING & MANAGEMENT ---
## Base path for all UI sound effects.
const SFX_DIR_PATH: String = "res://files/sounds/sfx/"

## Hard cap for cached SFX streams to prevent unbounded memory growth.
const MAX_SFX_CACHE_SIZE: int = 20

## Number of reusable AudioStreamPlayers to keep in memory for UI sounds.
const SFX_POOL_SIZE: int = 8

@export_category("Master Volume")
@export var master_volume: float
@export var master_muted: bool
Expand All @@ -32,14 +42,29 @@ signal mute_toggled(bus_name: String, is_muted: bool)

var current_config_path: String = Settings.CONFIG_PATH

# --- SFX CACHE STATE ---
## Dictionary to store preloaded AudioStreams to prevent disk I/O stutter.
var _sfx_cache: Dictionary = {}

## Dictionary acting as a set to track missing SFX and prevent repeated load attempts/log spam.
var _missing_sfx_cache: Dictionary = {}

## Array of pre-instantiated AudioStreamPlayers to prevent node instantiation churn.
var _sfx_pool: Array[AudioStreamPlayer] = []


func _ready() -> void:
## Initializes to defaults and loads/applies volumes.
## :rtype: void
_init_to_defaults() # Set to defaults from AudioConstants
load_volumes() # Load persisted volumes (overrides defaults if saved)
apply_all_volumes() # Apply to AudioServer buses

# Initialize the SFX object pool
for i in range(SFX_POOL_SIZE):
var p := AudioStreamPlayer.new()
add_child(p)
_sfx_pool.append(p)


## Initialize all volumes and mutes to defaults from AudioConstants
## :rtype: void
Expand Down Expand Up @@ -333,3 +358,81 @@ func reset_volumes() -> void:
apply_all_volumes()
save_volumes()
Globals.log_message("Audio volumes reset to defaults.", Globals.LogLevel.DEBUG)


## Centralized SFX Playback API (Issue #565)
## Handles non-positional audio with LRU caching and auto-cleanup.
## :param sfx_name: The filename without extension (e.g., "slider").
## :param bus_name: Target audio bus (defaults to SFX_Menu).
## :param pitch_scale: Pitch override for variety.
## :param volume_db: Volume offset in decibels.
func play_sfx(
sfx_name: String,
bus_name: String = AudioConstants.BUS_SFX_MENU,
pitch_scale: float = 1.0,
volume_db: float = 0.0
) -> void:
if sfx_name.is_empty():
return

# Short-circuit: If we already know this file is missing, do not attempt to load it again.
if _missing_sfx_cache.has(sfx_name):
return

# 1. Resolve and Cache the AudioStream (with LRU Eviction)
if not _sfx_cache.has(sfx_name):
var full_path: String = SFX_DIR_PATH + sfx_name + ".wav"
var stream: AudioStream = load(full_path)

if stream:
# Eviction strategy: If cache is full, remove the oldest (first) entry
if _sfx_cache.size() >= MAX_SFX_CACHE_SIZE:
var oldest_key: String = _sfx_cache.keys()[0]
_sfx_cache.erase(oldest_key)
Globals.log_message(
"SFX cache full. Evicted: " + oldest_key, Globals.LogLevel.DEBUG
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
)

_sfx_cache[sfx_name] = stream
else:
Globals.log_message(
"SFX file not found or failed to load: " + full_path, Globals.LogLevel.WARNING
)
# Cache the failure so we don't spam the disk and logs on subsequent requests
_missing_sfx_cache[sfx_name] = true
return
else:
# LRU Update: Godot 4 Dictionaries preserve insertion order.
# By erasing and re-inserting, we push this active sound to the "newest" end of the dictionary.
var stream: AudioStream = _sfx_cache[sfx_name]
_sfx_cache.erase(sfx_name)
_sfx_cache[sfx_name] = stream

# 2. Grab an available player from the object pool
var player: AudioStreamPlayer = null
for p: AudioStreamPlayer in _sfx_pool:
if not p.playing:
player = p
break

# Fallback: If all players are busy, hijack the first one in the pool
# to prevent dropping the new sound entirely.
if player == null:
player = _sfx_pool[0]

player.stream = _sfx_cache[sfx_name]
player.pitch_scale = pitch_scale
player.volume_db = volume_db

# 3. Bus Validation & Routing
if AudioServer.get_bus_index(bus_name) == -1:
Globals.log_message(
"Invalid bus '%s' requested for SFX. Falling back to SFX_Menu." % bus_name,
Globals.LogLevel.WARNING
)
player.bus = AudioConstants.BUS_SFX_MENU
else:
player.bus = bus_name

# 4. Play (No queue_free needed since we reuse the nodes)
player.play()
6 changes: 6 additions & 0 deletions scripts/resources/audio_constants.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@

extends Node

# --- Audio Bus Names ---
const BUS_MASTER: String = "Master"
const BUS_MUSIC: String = "Music"
const BUS_SFX: String = "SFX"
const BUS_SFX_ROTORS: String = "SFX_Rotors"
const BUS_SFX_WEAPON: String = "SFX_Weapon"
const BUS_SFX_MENU: String = "SFX_Menu"

# --- SFX Asset IDs ---
const SFX_SLIDER: String = "slider"
const SFX_MUTE_TOGGLE: String = "mute_toggle" # For future CheckButton task
const SFX_UI_NAVIGATION: String = "ui_navigation"

# Centralized config with defaults and var mappings
const BUS_CONFIG: Dictionary = {
BUS_MASTER:
Expand Down
Loading
Loading