diff --git a/ASSETS_LICENSES.md b/ASSETS_LICENSES.md index 4a5f81ba..f5f49bc0 100644 --- a/ASSETS_LICENSES.md +++ b/ASSETS_LICENSES.md @@ -53,6 +53,7 @@ These assets were made for this project, either by hand, by procedure, or with i | Bay Area Blitz music | `Audio/Platformer/Bay Area Blitz/` | Music by Erik Thorelli, made with Suno v6 for this game. Erik holds the rights and grants their use in the game and in this repository. Prompts, Suno song IDs, loop points and delivery steps in `PROMPTS.md`. | ✅ | | Orbital Ops music | `Audio/Platformer/Obital Ops/` | Music by Erik Thorelli, made with Suno v6 for this game. Erik holds the rights and grants their use in the game and in this repository. Prompts, Suno song IDs, loop points and delivery steps in `PROMPTS.md`. | ✅ | | Hoppy 3D mascot | `runner/hoppy/` | Built by `tools/blender/build_hoppy.py` | ⚠️ brand sign-off | +| Platformer stage-clear stinger | `Audio/Platformer/stinger_stage_clear.ogg` | Music by Erik Thorelli, made with Suno v6 for this game. Erik holds the rights and grants their use in the game and in this repository. Prompt, Suno song ID and edit in `Audio/Platformer/PROMPTS.md`. | ✅ | | Runner props, previews and procedural assets | `runner/` | See [`runner/LICENSES.md`](hoppys-adventure-arcade/Assets/runner/LICENSES.md) | ✅ | | Bay Area Blitz pack | `bay-area-blitz-assets-only/` | Image generation | ⚠️ confirm the generator's terms allow redistribution | | Orbital Ops pack | `orbital-ops-assets-only/` | Image generation | ⚠️ same | diff --git a/docs/bay-area-blitz.md b/docs/bay-area-blitz.md index 02746ea0..8f511c6a 100644 --- a/docs/bay-area-blitz.md +++ b/docs/bay-area-blitz.md @@ -11,7 +11,7 @@ Run the project normally: **Platformer Game → Level Select → Play Bay Area B - Swipe: J/X, controller X/Square. Landing on an enemy also deals damage and bounces Hoppy. - Pause/resume: Escape or controller Start. Focus loss pauses the game. - Defeat the Merge Conflict Monster, then swipe or land on the MERGE button to finish. -- Pause and results provide fresh retry, platformer menu, and arcade return. +- Pause and results provide fresh retry, platformer menu, and arcade return. Finishing a level (any of the three) plays a short stage-clear stinger (`Assets/Audio/Platformer/stinger_stage_clear.ogg`) over the MERGE SUCCESSFUL results while the paused level music stays silent; a knockout keeps its ducked level music instead. ## Implemented diff --git a/docs/soundtrack.md b/docs/soundtrack.md index 0c45449a..409f174a 100644 --- a/docs/soundtrack.md +++ b/docs/soundtrack.md @@ -122,6 +122,7 @@ Shared settings: Instrumental on, Variety Off. The lyrics field holds section ta | **Villain boss theme** (shared, as the GDD specifies) | All Heroes vs. Villains bosses | 1:00+ loop, strong hit in the first second | `Saturday-morning cartoon villain battle, 16-bit FM chiptune, 232 BPM double-time feel, minor key, campy menace, orchestra-hit stabs, chromatic runs, motif in minor, huge downbeat in the first second, loopable` | | **Opening fanfare** | Heroes vs. Villains intro | 10–15 s, hard stop | Not 8-bit: the theme's chorus and gang shout, cut from track A. | | **MERGE SUCCESSFUL** | Code City finale, ending and results | 3.5 s one-shot, then a 16.552 s loop | Delivered: original F-major square-wave synthesis at 116 BPM; rising F-A-C motif resolving to F. See the reproducible build below. | +| **Platformer stage clear** | Platformer MERGE SUCCESSFUL results | 4.7 s, one-shot | Delivered: `Assets/Audio/Platformer/stinger_stage_clear.ogg`, a Suno Sounds one-shot in F major; see that folder's `PROMPTS.md`. Plays on the Music bus while the results pause the level music. | | **BUILD FAILED** | Lose results | 2–3 s, one-shot | Sounds, One-Shot: `8-bit comic fail sting, three-note motif descending in minor, wobbly square lead` | | **Capability unlocked** (×4) | REVIEW / TRIAGE / CHANGE STACK / SECURITY cards | 1–2 s, one-shot | Sounds, One-Shot: `chiptune power-up chime, three-note glockenspiel-like square arpeggio` (one take per capability, rising in pitch) | | **BOSS DOWN!** | Boss defeat | about 2 s | Keep Juhani Junkala's CC0 `fanfare3`, or a Sounds one-shot of the motif. | diff --git a/hoppys-adventure-arcade/Assets/Audio/Platformer/PROMPTS.md b/hoppys-adventure-arcade/Assets/Audio/Platformer/PROMPTS.md new file mode 100644 index 00000000..535b8958 --- /dev/null +++ b/hoppys-adventure-arcade/Assets/Audio/Platformer/PROMPTS.md @@ -0,0 +1,21 @@ +# Platformer stage-clear stinger + +Music by Erik Thorelli, made with Suno v6 on his Premier plan for Hoppy's Adventure Arcade. Erik holds the rights and grants their use in the game and in this repository. The soundtrack plan is [`docs/soundtrack.md`](../../../../docs/soundtrack.md); level music has its own `PROMPTS.md` in each level's folder. + +| File | Plays | Suno song | Measured | Length | +|---|---|---|---|---| +| `stinger_stage_clear.ogg` | Once, when any platformer level finishes and the MERGE SUCCESSFUL results open (`modes/platformer/level.gd`, `_finish(true)`) | [`73fc8220-4010-43da-b82f-95e63862062e`](https://suno.com/song/73fc8220-4010-43da-b82f-95e63862062e) | F major, the theme's key, at 116 BPM | 4.74 s, one-shot | + +## Prompt + +Suno Sounds, One-Shot, BPM 116, key F major: + +```text +chiptune level-clear jingle, bouncy two-bar fanfare, three-note rising motif F A C then a held tonic chord hit, square-wave lead with triangle bass and a noise-channel drum fill, cheerful arcade stage clear, ends cleanly, about 4 seconds +``` + +Generated and downloaded as WAV on October 15, 2026. Takes compared: this one and `4cc54a58-bf2a-44a4-8e24-ae2010b2de84`. This one is clearly in F major (Krumhansl fit 0.94; the other reads as C major) and scored higher on Meta's audiobox-aesthetics model (content enjoyment 6.38 against 6.08, production quality 7.59 against 7.12). + +## Delivery + +Suno returned an 8.2 s phrase. It was raised 1.5 dB to −14 LUFS and cut to its closing phrase, the two bars before the final decay (3.415–8.154 s in the take), keeping the natural decay. A 4 ms fade-in and a 30 ms fade-out were applied, then the cut was encoded with `oggenc -q 6`, keeping Suno's embedded comment with the song ID. The `.import` leaves `loop` off. diff --git a/hoppys-adventure-arcade/Assets/Audio/Platformer/stinger_stage_clear.ogg b/hoppys-adventure-arcade/Assets/Audio/Platformer/stinger_stage_clear.ogg new file mode 100644 index 00000000..df859e59 Binary files /dev/null and b/hoppys-adventure-arcade/Assets/Audio/Platformer/stinger_stage_clear.ogg differ diff --git a/hoppys-adventure-arcade/Assets/Audio/Platformer/stinger_stage_clear.ogg.import b/hoppys-adventure-arcade/Assets/Audio/Platformer/stinger_stage_clear.ogg.import new file mode 100644 index 00000000..c10a8ed6 --- /dev/null +++ b/hoppys-adventure-arcade/Assets/Audio/Platformer/stinger_stage_clear.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://qpafxw1argjw" +path="res://.godot/imported/stinger_stage_clear.ogg-f6facadb7bc526d5171cf1e53fcea724.oggvorbisstr" + +[deps] + +source_file="res://Assets/Audio/Platformer/stinger_stage_clear.ogg" +dest_files=["res://.godot/imported/stinger_stage_clear.ogg-f6facadb7bc526d5171cf1e53fcea724.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/hoppys-adventure-arcade/modes/platformer/level.gd b/hoppys-adventure-arcade/modes/platformer/level.gd index 490987af..1224760d 100644 --- a/hoppys-adventure-arcade/modes/platformer/level.gd +++ b/hoppys-adventure-arcade/modes/platformer/level.gd @@ -12,6 +12,8 @@ const SFX := { "hurt": preload("res://Assets/Backgrounds/Sunny-land-forest-files/demo/assets/sound/wavs/hurt.wav"), "boss_down": preload("res://Assets/Audio/The Essential Retro Video Game Sound Effects Collection [512 sounds] By Juhani Junkala/General Sounds/Fanfares/sfx_sounds_fanfare3.wav") } +## Plays once over the MERGE SUCCESSFUL results, while the paused level music is silent. +const STAGE_CLEAR := preload("res://Assets/Audio/Platformer/stinger_stage_clear.ogg") const EXIT_TOP := 540.0 const PAUSE_SHADE := Color(0.025, 0.06, 0.1, 0.96) # Lighter than the pause shade so the frozen final moment stays readable behind the results. @@ -53,6 +55,7 @@ var overlay_card: Control var results_reveal: Tween var exit_sprite: AnimatedSprite2D var music: AudioStreamPlayer +var stage_clear: AudioStreamPlayer var sfx_players := {} var encounter := "approach" var intro_remaining := 0.0 @@ -222,6 +225,8 @@ func _hud() -> void: var button := Style.button(entry[0]) col.add_child(button) button.pressed.connect(func(): + # Leaving the results unpauses the level music; end the stinger before they overlap. + stage_clear.stop() get_tree().paused = false AppRouter.go(entry[1])) overlay.hide() @@ -266,6 +271,7 @@ func _finish(won: bool) -> void: overlay_detail.text = "%06d POINTS / %02d:%02d\n%s" % [score, int(elapsed) / 60, int(elapsed) % 60, level_title + " complete." if won else "Fresh branch. Another try."] resume_button.hide() if won: + stage_clear.play() _reveal_results(0.35) else: _start_knockout() @@ -506,5 +512,12 @@ func _setup_session() -> void: sfx_players[clip] = sound for child in get_children(): if not child is CanvasLayer: child.process_mode = Node.PROCESS_MODE_PAUSABLE + # The results pause the tree, so the stage-clear stinger has to keep processing. + stage_clear = AudioStreamPlayer.new() + stage_clear.bus = "Music" + stage_clear.stream = STAGE_CLEAR + stage_clear.volume_db = -13 + stage_clear.process_mode = Node.PROCESS_MODE_ALWAYS + add_child(stage_clear) # Everything above was placed, not moved: start interpolation from those positions. reset_physics_interpolation() diff --git a/hoppys-adventure-arcade/tests/platformer_stage_clear.gd b/hoppys-adventure-arcade/tests/platformer_stage_clear.gd new file mode 100644 index 00000000..ac182c09 --- /dev/null +++ b/hoppys-adventure-arcade/tests/platformer_stage_clear.gd @@ -0,0 +1,56 @@ +extends SceneTree +## Finishing any platformer level plays the stage-clear stinger once over the MERGE SUCCESSFUL +## results: it keeps playing while the results pause the tree, the paused level music stays +## silent under it, and a knockout never plays it. Run headless with +## --script res://tests/platformer_stage_clear.gd. +const LEVELS := ["bay_area_blitz", "enchanted_repo", "orbital_ops"] +const STINGER := "res://Assets/Audio/Platformer/stinger_stage_clear.ogg" +var failures := 0 +var game: Node + +func _initialize() -> void: + run.call_deferred() + +func check(ok: bool, message: String) -> void: + print("PASS: " if ok else "FAIL: ", message) + if not ok: failures += 1 + +func open_level(route: String) -> void: + change_scene_to_file("res://scenes/platformer/" + route + ".tscn") + await process_frame + await process_frame + game = current_scene + game._resume() + await create_timer(0.2).timeout + +func run() -> void: + var stream := load(STINGER) as AudioStreamOggVorbis + check(stream != null and not stream.loop and stream.get_length() > 2.0 and stream.get_length() < 6.0, + "The stage-clear stinger is a short one-shot") + for route in LEVELS: + await open_level(route) + check(game.stage_clear.stream == stream and not game.stage_clear.playing, route + ": the stinger waits for the finish") + game._finish(true) + await create_timer(0.5, true, false, true).timeout + check(paused and game.stage_clear.playing and game.stage_clear.bus == &"Music", + route + ": MERGE SUCCESSFUL plays the stinger on the Music bus while the results pause the tree") + check(game.music.process_mode != Node.PROCESS_MODE_ALWAYS and game.music.can_process() == false, + route + ": the level music stays paused under the stinger") + var leave := game.find_children("*", "Button", true, false).filter(func(b: Button) -> bool: return b.text == "Return to Platformer Menu") + check(not leave.is_empty(), route + ": the results offer a way out") + if not leave.is_empty(): + (leave[0] as Button).pressed.emit() + check(not game.stage_clear.playing, route + ": leaving the results stops the stinger before the level music resumes") + await create_timer(0.6, true, false, true).timeout + paused = false + await open_level(route) + game.player.position.y = 900 + game.player._physics_process(0.0) + await create_timer(0.5, true, false, true).timeout + check(not game.stage_clear.playing, route + ": a knockout does not play the stage-clear stinger") + paused = false + print("PLATFORMER STAGE CLEAR: %s (%d failures)" % ["PASS" if failures == 0 else "FAIL", failures]) + change_scene_to_file("res://scenes/main_menu/main_menu.tscn") + await process_frame + await root.get_node("AudioService").settle() + quit(1 if failures else 0) diff --git a/hoppys-adventure-arcade/tests/platformer_stage_clear.gd.uid b/hoppys-adventure-arcade/tests/platformer_stage_clear.gd.uid new file mode 100644 index 00000000..091ceb05 --- /dev/null +++ b/hoppys-adventure-arcade/tests/platformer_stage_clear.gd.uid @@ -0,0 +1 @@ +uid://cd5vqgnbgwu5e