diff --git a/Game/scripts/game_mode.gd b/Game/scripts/game_mode.gd index a403928f..60417adf 100644 --- a/Game/scripts/game_mode.gd +++ b/Game/scripts/game_mode.gd @@ -175,3 +175,37 @@ func _reset_body(body: RigidBody3D, to: Transform3D) -> void: func _unhandled_input(event): if event.is_action_pressed("ui_cancel"): get_tree().change_scene_to_file(ScenePaths.MAIN_MENU) + + +# Escape failsafe. The arena is meant to be fully enclosed, but the goal +# mouths are now a real navigable hole in the end walls (see +# ArenaBoundary._build_end_wall_colliders) sized to the ball, not the ship — +# a ship's 1x1 cross-section fits through it, and there is nothing behind the +# net to stop it. Previously this only existed in TrainingMode (a physics +# regression there just wastes training time); now that any ship can +# genuinely fly out through an open goal, every mode needs it, or a stray +# ship/ball falls into the void with no way back short of quitting. Runs by +# default every tick; TrainingMode overrides _physics_process entirely and +# calls this itself alongside its own episode logic. +const ESCAPE_MARGIN := 15.0 + + +func _physics_process(_delta: float) -> void: + _respawn_escaped_bodies() + + +func _respawn_escaped_bodies() -> void: + for ship in ships: + if is_instance_valid(ship) and _is_escaped(ship.global_position): + push_warning("GameMode: ship escaped the enclosed arena — check boundary colliders") + _reset_body(ship, _ship_spawn_transforms[ship]) + if is_instance_valid(ball) and _is_escaped(ball.global_position): + push_warning("GameMode: ball escaped the enclosed arena — check boundary colliders") + _reset_body(ball, arena.get_ball_spawn()) + + +func _is_escaped(position: Vector3) -> bool: + return absf(position.x) > ArenaBoundary.INNER_HALF_X + ESCAPE_MARGIN \ + or absf(position.z) > ArenaBoundary.INNER_HALF_Z + ESCAPE_MARGIN \ + or position.y < -ESCAPE_MARGIN \ + or position.y > ArenaBoundary.INNER_HEIGHT + ESCAPE_MARGIN diff --git a/Game/scripts/training_mode.gd b/Game/scripts/training_mode.gd index d2b202f1..ab19fd9e 100644 --- a/Game/scripts/training_mode.gd +++ b/Game/scripts/training_mode.gd @@ -90,12 +90,6 @@ const MAX_RANDOM_SHIP_SPEED := 8.0 # Sim runs at 60 physics ticks per sim-second regardless of speedup. const TICKS_PER_SIM_SECOND := 60.0 -# The arena is physically enclosed, so nothing should ever get this far out. -# If a body escapes anyway (physics regression, boundary edit), it is warned -# about and respawned with no reward change and no episode end — a multi-hour -# training run must survive it, and the escape must not shape rewards. -const ESCAPE_MARGIN := 15.0 - var _agents: Array[ShipAIController] = [] # Eval mode state (see header comment) @@ -320,24 +314,6 @@ func _physics_process(_delta): return -# Escape failsafe: see ESCAPE_MARGIN. -func _respawn_escaped_bodies() -> void: - for ship in ships: - if is_instance_valid(ship) and _escaped(ship.global_position): - push_warning("TrainingMode: ship escaped the enclosed arena — check boundary colliders") - _place_body(ship, _ship_spawn_transforms[ship], Vector3.ZERO, Vector3.ZERO) - if is_instance_valid(ball) and _escaped(ball.global_position): - push_warning("TrainingMode: ball escaped the enclosed arena — check boundary colliders") - _place_body(ball, arena.get_ball_spawn(), Vector3.ZERO, Vector3.ZERO) - - -func _escaped(position: Vector3) -> bool: - return absf(position.x) > ArenaBoundary.INNER_HALF_X + ESCAPE_MARGIN \ - or absf(position.z) > ArenaBoundary.INNER_HALF_Z + ESCAPE_MARGIN \ - or position.y < -ESCAPE_MARGIN \ - or position.y > ArenaBoundary.INNER_HEIGHT + ESCAPE_MARGIN - - func _on_goal_scored(conceding_team: int) -> void: if _eval: _eval_goals[1 - conceding_team] += 1