From c96325144adef94d22c85edac434221b21755364 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:35:41 +0100 Subject: [PATCH] fix(match): guard kickoff countdown against a post-match resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The coroutine stalls mid-countdown while the tree is paused for the results screen, but _end_match unpauses before the deferred scene change actually tears things down — letting it resume for a frame and re-emit kickoff_countdown / unfreeze bodies in the dying scene. --- Game/scripts/match_mode.gd | 12 +++++++++++- TODO.md | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Game/scripts/match_mode.gd b/Game/scripts/match_mode.gd index 8df4c8c8..d8219f16 100644 --- a/Game/scripts/match_mode.gd +++ b/Game/scripts/match_mode.gd @@ -29,6 +29,7 @@ const KICKOFF_COUNTDOWN_SECONDS := 3 var match_timer: Timer var _in_overtime := false +var _match_over := false func _get_arena_scene_path() -> String: @@ -67,6 +68,8 @@ func _process(_delta): func _on_goal_scored(conceding_team: int) -> void: + if _match_over: + return var scoring_team := 1 - conceding_team _record_goal(scoring_team) score_changed.emit(score.duplicate()) @@ -82,6 +85,8 @@ func _on_goal_scored(conceding_team: int) -> void: # changes to ship.gd/ball.gd/ship_controller.gd, keeping training_mode.gd # (which never calls into this file) completely untouched. func _run_kickoff_countdown() -> void: + if _match_over: + return reset_ball() reset_ships() _set_frozen(true) @@ -90,8 +95,12 @@ func _run_kickoff_countdown() -> void: # process_always=false: if full-time fires mid-countdown (see # _on_match_timer_timeout's get_tree().paused = true), this stalls # harmlessly in lockstep with the pause instead of ticking a - # countdown label over the results screen. + # countdown label over the results screen. _end_match unpauses the + # tree before the scene teardown actually lands, so the _match_over + # check below is what stops this from resuming into a dying scene. await get_tree().create_timer(1.0, false).timeout + if _match_over: + return kickoff_countdown.emit(0) _set_frozen(false) @@ -121,6 +130,7 @@ func _start_overtime() -> void: func _end_match(winning_team: int) -> void: + _match_over = true print("Match over! Final score: %d - %d" % [score[0], score[1]]) match_ended.emit(winning_team, score.duplicate()) # Freeze gameplay while the HUD (process_mode ALWAYS) shows the result. diff --git a/TODO.md b/TODO.md index 480a2219..8cf13cc6 100644 --- a/TODO.md +++ b/TODO.md @@ -17,7 +17,7 @@ Bugs found in an adversarial review. None are gameplay- or physics-affecting, so - [x] `VideoSettings.apply_to_environment()` (`scripts/video_settings.gd`) compounds on every arena load: `env.glow_intensity *= glow_scale` mutates an `Environment` that is a `[sub_resource]` of the arena scene, and Godot shares sub-resources across instantiations of a cached `PackedScene`. Glow at 50% becomes 25% then 12.5% across repeat entries. Fix by duplicating the Environment in `Arena._ready()` (`scripts/arena.gd`). Regression test: set glow to 50%, enter/leave Free Play three times, confirm it's still 50%. - [x] Collapse the four disagreeing team palettes into one source of truth — `ship.gd`, `HUDController.gd` and `goal.gd` each declare `TEAM_COLORS`, `arena_boundary.gd` exports `team0_tint`/`team1_tint`, and `arena_deck.gdshader` defaults to a fifth pair. Three of them disagree, so nose, goal rim, end zone and scoreboard are all different blues. - [ ] Goal scoring volume (3.5 x 1.5, `objects/goal.tscn`) is smaller than the drawn mouth (3.7 x 1.65, `ArenaBoundary.GOAL_APERTURE_*`) — a ball crossing the visible edge doesn't score. Derive the aperture constants from the goal's collision shape, the way `goal.gd:53` already measures its own visuals. -- [ ] `match_mode.gd`: full time can fire mid-kickoff-countdown, and the stalled coroutine resumes into the dying scene (can re-emit `kickoff_countdown` / unfreeze bodies for a frame). Guard `_run_kickoff_countdown` with a match-over flag. +- [x] `match_mode.gd`: full time can fire mid-kickoff-countdown, and the stalled coroutine resumes into the dying scene (can re-emit `kickoff_countdown` / unfreeze bodies for a frame). Guard `_run_kickoff_countdown` with a match-over flag. - [ ] `match_mode.gd` emits `timer_updated` every frame for a value that changes once a second; the HUD re-formats and re-shapes the label each time. Emit only on change, matching `ship.gd`'s threshold-gated telemetry discipline. - [ ] `HUDController` binds to `get_first_node_in_group("ship")` in a group that always has 2+ members — works only because the player ship happens to spawn first. Have the game mode hand the HUD its target ship. - [ ] Reuse a member `ShipAction` in `ship.gd` (controllerless path) and `player_ship_controller.gd` instead of allocating one per physics tick; `ai_ship_controller.gd` already does this correctly.