diff --git a/README.md b/README.md index 60c0f08c..a757dd25 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 c7bbe137..360ea819 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 69aa65b3..cf77e1c0 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 or P. 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. Entering the level from the menus opens its [how-to-play card](main-menu.md#how-to-play-cards); the pause menu's **How to Play** shows it again. diff --git a/docs/orbital-ops.md b/docs/orbital-ops.md index b0ea08be..2b7d97c2 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 4163d9d4..39a216f2 100644 --- a/hoppys-adventure-arcade/tests/platformer_smoke.gd +++ b/hoppys-adventure-arcade/tests/platformer_smoke.gd @@ -12,6 +12,16 @@ 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") + # 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 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() @@ -198,11 +208,29 @@ 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") check(root.get_node("AudioService").music.process_mode == Node.PROCESS_MODE_INHERIT, "The hub music pauses with the tree again outside the level") + # 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 + # 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) await root.get_node("AudioService").settle()