From 3d993378949c349bc6bc132c49c2f9dcc6d26a10 Mon Sep 17 00:00:00 2001 From: Juan Pa Date: Sun, 8 Nov 2026 18:42:56 -0800 Subject: [PATCH 1/4] fix(platformer): open straight on Level Select The platformer's title screen (Level Select / Return to Main Menu / Quit Game) repeated the hub's own options. Choosing Platformer now lands on Level Select, whose back button and Esc return to the main menu. --- README.md | 2 +- docs/bay-area-blitz.md | 2 +- docs/enchanted-repo.md | 2 +- docs/orbital-ops.md | 2 +- .../scenes/platformer/platformer_menu.gd | 30 ++++--------------- .../tests/platformer_smoke.gd | 2 +- 6 files changed, 10 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index daac46bc..704c98f4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A Godot 4 arcade collection starring Hoppy, the CodeRabbit mascot. It has a reac ```sh godot --path hoppys-adventure-arcade ``` -3. Choose **Platformer Game → Level Select**, **Endless Runner**, or **Beat’em Up → Play Code City Brawl / Play Heroes vs. Villains**. +3. Choose **Platformer Game** (it opens on Level Select), **Endless Runner**, or **Beat’em Up → Play Code City Brawl / Play Heroes vs. Villains**. | Action | Keyboard | Controller | |---|---|---| diff --git a/docs/bay-area-blitz.md b/docs/bay-area-blitz.md index 8f511c6a..b8f4f580 100644 --- a/docs/bay-area-blitz.md +++ b/docs/bay-area-blitz.md @@ -4,7 +4,7 @@ Implemented September 20, 2026 against Godot 4.7.1. Design references: the share ## Play -Run the project normally: **Platformer Game → Level Select → Play Bay Area Blitz**. For direct iteration, run `res://scenes/platformer/bay_area_blitz.tscn`. +Run the project normally: **Platformer Game → Play Bay Area Blitz**. For direct iteration, run `res://scenes/platformer/bay_area_blitz.tscn`. - Move: A/D, left/right arrows, controller left stick or D-pad. - Jump: Space/W/up arrow, controller A/Cross. Release early for a shorter jump. diff --git a/docs/enchanted-repo.md b/docs/enchanted-repo.md index b982b83c..7663b7e7 100644 --- a/docs/enchanted-repo.md +++ b/docs/enchanted-repo.md @@ -4,7 +4,7 @@ Implemented in Godot 4.7.1 from the current shared GDD's Platformer section, the ## Play -Choose **Platformer Game → Level Select → Play Enchanted Repo**. Direct scene: `res://scenes/platformer/enchanted_repo.tscn`. +Choose **Platformer Game → Play Enchanted Repo**. Direct scene: `res://scenes/platformer/enchanted_repo.tscn`. Move with A/D or arrows, jump with Space/W/up, swipe with J/X, pause with Escape. Existing gamepad mappings remain stick/D-pad movement, A/Cross jump, X/Square attack, and Start pause. Release Jump early for a short hop. Descending onto an enemy bounces Hoppy; the Monolith takes damage only in its recovery windows. diff --git a/docs/orbital-ops.md b/docs/orbital-ops.md index ed76cdd4..ee2002c6 100644 --- a/docs/orbital-ops.md +++ b/docs/orbital-ops.md @@ -4,7 +4,7 @@ Implemented October 1, 2026 in Godot 4.7.1. Source references: the current share ## Play -Launch the project, choose **Platformer Game → Level Select → Play Orbital Ops**. Direct scene: `res://scenes/platformer/orbital_ops.tscn`. +Launch the project, choose **Platformer Game → Play Orbital Ops**. Direct scene: `res://scenes/platformer/orbital_ops.tscn`. - Move: A/D, left/right arrows, gamepad stick or D-pad. - Jump: Space/W/up, gamepad A/Cross. Release early for a shorter normal-gravity jump. diff --git a/hoppys-adventure-arcade/scenes/platformer/platformer_menu.gd b/hoppys-adventure-arcade/scenes/platformer/platformer_menu.gd index ec0366b2..f587a9e7 100644 --- a/hoppys-adventure-arcade/scenes/platformer/platformer_menu.gd +++ b/hoppys-adventure-arcade/scenes/platformer/platformer_menu.gd @@ -1,6 +1,5 @@ extends Control const Style = preload("res://scenes/main_menu/menu_style.gd") -var selecting := false var column: VBoxContainer func _ready() -> void: theme = Style.theme() @@ -22,32 +21,14 @@ func _ready() -> void: column = VBoxContainer.new() column.add_theme_constant_override("separation", 20) margin.add_child(column) - _main() + _select() func _clear() -> void: for child in column.get_children(): column.remove_child(child) child.queue_free() -func _main() -> void: - selecting = false - _clear() - column.add_child(Style.label("HOPPY'S ADVENTURE ARCADE / 01", 16, Style.MINT)) - column.add_child(Style.label("PLATFORMER", 52)) - column.add_child(Style.label("Small rabbit. Big commits.", 22)) - column.add_child(Style.label("Cross the city, explore orbit, or brave the enchanted forest. Clear the blockers and ship the build.", 17)) - var select := Style.button("Level Select →") - column.add_child(select) - select.pressed.connect(_select) - var back := Style.button("Return to Main Menu") - column.add_child(back) - back.pressed.connect(func(): AppRouter.go("hub")) - var quit := Style.button("Quit Game") - column.add_child(quit) - quit.pressed.connect(func(): AppRouter.quit_game()) - select.grab_focus() func _select() -> void: - selecting = true _clear() - column.add_child(Style.label("CHOOSE YOUR NEXT COMMIT", 16, Style.MINT)) + column.add_child(Style.label("HOPPY'S ADVENTURE ARCADE / 01 · PLATFORMER", 16, Style.MINT)) column.add_child(Style.label("LEVEL SELECT", 42)) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 22) @@ -86,10 +67,9 @@ func _select() -> void: play.pressed.connect(func(): AppRouter.go("orbital_ops")) else: play.pressed.connect(func(): AppRouter.go("enchanted_repo")) - var back := Style.button("← Back") + var back := Style.button("← Main Menu") column.add_child(back) - back.pressed.connect(_main) + back.pressed.connect(func(): AppRouter.go("hub")) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): - if selecting: _main() - else: AppRouter.go("hub") + AppRouter.go("hub") diff --git a/hoppys-adventure-arcade/tests/platformer_smoke.gd b/hoppys-adventure-arcade/tests/platformer_smoke.gd index 6f897efd..7f6168fa 100644 --- a/hoppys-adventure-arcade/tests/platformer_smoke.gd +++ b/hoppys-adventure-arcade/tests/platformer_smoke.gd @@ -176,7 +176,7 @@ func run() -> void: paused = false root.get_node("AppRouter").go("platformer") await create_timer(0.7).timeout - current_scene._select() + check(current_scene.column.get_child(1).text == "LEVEL SELECT", "Platformer opens straight on Level Select") await frames(3) await capture("/tmp/hoppy-level-select.png") check(not root.get_node("AudioService").music.stream_paused, "Menu music restored") From 922db8ca3e18762e2563fd2c186b7a85bf527f10 Mon Sep 17 00:00:00 2001 From: Juan Pa Date: Sun, 8 Nov 2026 19:29:32 -0800 Subject: [PATCH 2/4] test(platformer): cover both ways back to the hub from Level Select MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Press ← Main Menu, then reopen the platformer and send ui_cancel; each must land on the hub. Waits are bounded and also wait for the router to finish its fade, since it refuses a new route while busy. --- .../tests/platformer_smoke.gd | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/hoppys-adventure-arcade/tests/platformer_smoke.gd b/hoppys-adventure-arcade/tests/platformer_smoke.gd index 7f6168fa..17cac8e5 100644 --- a/hoppys-adventure-arcade/tests/platformer_smoke.gd +++ b/hoppys-adventure-arcade/tests/platformer_smoke.gd @@ -12,6 +12,14 @@ func capture(path: String) -> void: if DisplayServer.get_name() != "headless": await RenderingServer.frame_post_draw root.get_texture().get_image().save_png(path) +## Waits up to 3 s for the router to open `path` and finish its fade (it refuses routes while busy). +func settles_on(path: String) -> bool: + var router := root.get_node("AppRouter") + for i in 180: + if current_scene != null and current_scene.scene_file_path == path and not router.busy: + return true + await process_frame + return false func run() -> void: for binding in [["move_left", KEY_LEFT], ["move_right", KEY_RIGHT], ["jump", KEY_UP]]: var key := InputEventKey.new() @@ -180,6 +188,20 @@ func run() -> void: await frames(3) await capture("/tmp/hoppy-level-select.png") check(not root.get_node("AudioService").music.stream_paused, "Menu music restored") + # Both ways back from Level Select land on the hub. + var back: Button = current_scene.column.get_child(current_scene.column.get_child_count() - 1) + check(back.text == "← Main Menu", "Level Select offers the way back to the hub") + back.pressed.emit() + check(await settles_on("res://scenes/main_menu/main_menu.tscn"), "← Main Menu returns to the hub") + check(await root.get_node("AppRouter").go("platformer") and await settles_on("res://scenes/platformer/platformer_menu.tscn"), "Platformer reopens on Level Select") + if current_scene.scene_file_path != "res://scenes/platformer/platformer_menu.tscn": + quit(failures) + return + var cancel := InputEventAction.new() + cancel.action = "ui_cancel" + cancel.pressed = true + current_scene._unhandled_input(cancel) + check(await settles_on("res://scenes/main_menu/main_menu.tscn"), "ui_cancel on Level Select returns to the hub") print("PLATFORMER SMOKE: ", "PASS" if failures == 0 else "FAIL", " (", failures, " failures)") await frames(35) await root.get_node("AudioService").settle() From 00092f74e7329dbbbbf9554517ba125d58fcfa8e Mon Sep 17 00:00:00 2001 From: Juan Pa Date: Sun, 8 Nov 2026 20:09:36 -0800 Subject: [PATCH 3/4] test(platformer): send ui_cancel through Input dispatch Calling _unhandled_input directly skipped the GUI and tree input path, so a focused control that consumed ui_cancel would not have failed the check. --- hoppys-adventure-arcade/tests/platformer_smoke.gd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hoppys-adventure-arcade/tests/platformer_smoke.gd b/hoppys-adventure-arcade/tests/platformer_smoke.gd index 17cac8e5..2289056c 100644 --- a/hoppys-adventure-arcade/tests/platformer_smoke.gd +++ b/hoppys-adventure-arcade/tests/platformer_smoke.gd @@ -200,7 +200,11 @@ func run() -> void: var cancel := InputEventAction.new() cancel.action = "ui_cancel" cancel.pressed = true - current_scene._unhandled_input(cancel) + # Real input dispatch, so a focused control that swallowed ui_cancel would fail this check. + Input.parse_input_event(cancel) + var release := cancel.duplicate() as InputEventAction + release.pressed = false + Input.parse_input_event(release) check(await settles_on("res://scenes/main_menu/main_menu.tscn"), "ui_cancel on Level Select returns to the hub") print("PLATFORMER SMOKE: ", "PASS" if failures == 0 else "FAIL", " (", failures, " failures)") await frames(35) From 1a38f3759fe7b8d13784620382c263167803c43e Mon Sep 17 00:00:00 2001 From: Juan Pa Date: Mon, 9 Nov 2026 15:35:13 -0800 Subject: [PATCH 4/4] test(platformer): bound the route wait by wall-clock time 180 headless frames can pass before the router's 0.5 s of fades end, so settles_on() could report a failure for a route that succeeded. Wait on a 3 s Time.get_ticks_msec() deadline instead. --- hoppys-adventure-arcade/tests/platformer_smoke.gd | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hoppys-adventure-arcade/tests/platformer_smoke.gd b/hoppys-adventure-arcade/tests/platformer_smoke.gd index 60614028..39a216f2 100644 --- a/hoppys-adventure-arcade/tests/platformer_smoke.gd +++ b/hoppys-adventure-arcade/tests/platformer_smoke.gd @@ -15,11 +15,13 @@ func capture(path: String) -> void: ## Waits up to 3 s for the router to open `path` and finish its fade (it refuses routes while busy). func settles_on(path: String) -> bool: var router := root.get_node("AppRouter") - for i in 180: + # A wall-clock deadline: headless frames can run far faster than the router's 0.5 s of fades. + var deadline := Time.get_ticks_msec() + 3000 + while Time.get_ticks_msec() < deadline: if current_scene != null and current_scene.scene_file_path == path and not router.busy: return true await process_frame - return false + return current_scene != null and current_scene.scene_file_path == path and not router.busy func run() -> void: for binding in [["move_left", KEY_LEFT], ["move_right", KEY_RIGHT], ["jump", KEY_UP]]: var key := InputEventKey.new()