From df3e168b3162b28e5953a48c7484408a9403b4be Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:36:19 +0100 Subject: [PATCH] feat(arena): add elevated-goal arena variants Each of the three arenas gains an ELEVATED sibling scene that inherits the base arena and overrides the boundary's goal_mode, the two goal transforms and the ship spawns, so the goal sits at mid-wall height instead of flush with the deck. Registered in ArenaRegistry alongside the floor-level arenas, plus a training_elevated scene for self-play on them. TrainingMode reads the arena's goal_mode once in _start() and widens the ball-placement height range to match the goal's real position; on FLOOR arenas the bound is a no-op, so floor-level training is unchanged. It is read in _start() rather than _ready() because TrainingMode has no _ready() override and GameMode._ready() is what discovers the arena first. Policies trained against floor-level goals are not expected to score on an elevated one, so the two are kept as separate arenas rather than a variant of the same entry. --- Game/scenes/arena_01_elevated.tscn | 20 ++++++++++++++++++++ Game/scenes/arena_02_elevated.tscn | 20 ++++++++++++++++++++ Game/scenes/arena_03_elevated.tscn | 20 ++++++++++++++++++++ Game/scenes/training_elevated.tscn | 15 +++++++++++++++ Game/scripts/arena_registry.gd | 24 ++++++++++++++++++------ Game/scripts/training_mode.gd | 13 ++++++++++++- 6 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 Game/scenes/arena_01_elevated.tscn create mode 100644 Game/scenes/arena_02_elevated.tscn create mode 100644 Game/scenes/arena_03_elevated.tscn create mode 100644 Game/scenes/training_elevated.tscn diff --git a/Game/scenes/arena_01_elevated.tscn b/Game/scenes/arena_01_elevated.tscn new file mode 100644 index 00000000..7780a84c --- /dev/null +++ b/Game/scenes/arena_01_elevated.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="PackedScene" path="res://scenes/arena_01.tscn" id="1_base"] + +[node name="Arena" instance=ExtResource("1_base")] + +[node name="Boundary" parent="." index="0"] +goal_mode = 1 + +[node name="GoalTeam0" parent="." index="5"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6.0, 18) + +[node name="GoalTeam1" parent="." index="6"] +transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 6.0, -18) + +[node name="Spawn1" parent="SpawnsTeam0" index="0"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.0, 2.8, 13.5) + +[node name="Spawn1" parent="SpawnsTeam1" index="0"] +transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -4.0, 2.8, -13.5) diff --git a/Game/scenes/arena_02_elevated.tscn b/Game/scenes/arena_02_elevated.tscn new file mode 100644 index 00000000..f7391f23 --- /dev/null +++ b/Game/scenes/arena_02_elevated.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="PackedScene" path="res://scenes/arena_02.tscn" id="1_base"] + +[node name="Arena" instance=ExtResource("1_base")] + +[node name="Boundary" parent="." index="0"] +goal_mode = 1 + +[node name="GoalTeam0" parent="." index="5"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6.0, 18) + +[node name="GoalTeam1" parent="." index="6"] +transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 6.0, -18) + +[node name="Spawn1" parent="SpawnsTeam0" index="0"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.0, 2.8, 13.5) + +[node name="Spawn1" parent="SpawnsTeam1" index="0"] +transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -4.0, 2.8, -13.5) diff --git a/Game/scenes/arena_03_elevated.tscn b/Game/scenes/arena_03_elevated.tscn new file mode 100644 index 00000000..5a5e949b --- /dev/null +++ b/Game/scenes/arena_03_elevated.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="PackedScene" path="res://scenes/arena_03.tscn" id="1_base"] + +[node name="Arena" instance=ExtResource("1_base")] + +[node name="Boundary" parent="." index="0"] +goal_mode = 1 + +[node name="GoalTeam0" parent="." index="5"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6.0, 18) + +[node name="GoalTeam1" parent="." index="6"] +transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 6.0, -18) + +[node name="Spawn1" parent="SpawnsTeam0" index="0"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.0, 2.8, 13.5) + +[node name="Spawn1" parent="SpawnsTeam1" index="0"] +transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -4.0, 2.8, -13.5) diff --git a/Game/scenes/training_elevated.tscn b/Game/scenes/training_elevated.tscn new file mode 100644 index 00000000..e0bc30ff --- /dev/null +++ b/Game/scenes/training_elevated.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://scripts/training_mode.gd" id="1_tr"] +[ext_resource type="PackedScene" path="res://scenes/arena_01_elevated.tscn" id="2_tr"] +[ext_resource type="Script" path="res://addons/godot_rl_agents/sync.gd" id="3_tr"] + +[node name="Training" type="Node3D"] +script = ExtResource("1_tr") + +[node name="Arena" parent="." instance=ExtResource("2_tr")] + +[node name="Sync" type="Node" parent="."] +script = ExtResource("3_tr") +action_repeat = 8 +speed_up = 8.0 diff --git a/Game/scripts/arena_registry.gd b/Game/scripts/arena_registry.gd index a5047878..d6855227 100644 --- a/Game/scripts/arena_registry.gd +++ b/Game/scripts/arena_registry.gd @@ -1,14 +1,26 @@ class_name ArenaRegistry # Single source of truth for available arenas: the Free Play menu dropdown -# lists these, and Match/Spectate pick one at random each session. Training -# is exempt — training.tscn keeps its own fixed arena_01 child. +# lists all of these, and Match/Spectate pick at random each session from +# those with "random" true. Training is exempt — training.tscn/ +# training_elevated.tscn each keep their own fixed Arena child. +# +# Elevated-goal variants are Free-Play-only ("random": false) until a +# checkpoint trained on training_elevated.tscn is promoted — the current +# promoted bots (see main_menu.gd's DIFFICULTIES) were trained exclusively +# on floor-level goals and can't be expected to score on an elevated one. +# Flip a variant's "random" flag to true once that training/promotion has +# happened. const ARENAS := [ - {"name": "Starfield", "path": "res://scenes/arena_01.tscn"}, - {"name": "Nebula", "path": "res://scenes/arena_02.tscn"}, - {"name": "Asteroid Field", "path": "res://scenes/arena_03.tscn"}, + {"name": "Starfield (Floor Goals)", "path": "res://scenes/arena_01.tscn", "random": true}, + {"name": "Starfield (Elevated Goals)", "path": "res://scenes/arena_01_elevated.tscn", "random": false}, + {"name": "Nebula (Floor Goals)", "path": "res://scenes/arena_02.tscn", "random": true}, + {"name": "Nebula (Elevated Goals)", "path": "res://scenes/arena_02_elevated.tscn", "random": false}, + {"name": "Asteroid Field (Floor Goals)", "path": "res://scenes/arena_03.tscn", "random": true}, + {"name": "Asteroid Field (Elevated Goals)", "path": "res://scenes/arena_03_elevated.tscn", "random": false}, ] static func random_path() -> String: - return ARENAS[randi() % ARENAS.size()]["path"] + var candidates := ARENAS.filter(func(arena): return arena["random"]) + return candidates[randi() % candidates.size()]["path"] diff --git a/Game/scripts/training_mode.gd b/Game/scripts/training_mode.gd index c8629767..50297ded 100644 --- a/Game/scripts/training_mode.gd +++ b/Game/scripts/training_mode.gd @@ -120,8 +120,16 @@ var _ai_overrides := {} # arena spawn instead of drifting into the play area as a stray obstacle. var _inert_ships: Array[Ship] = [] +# Whether this run's arena is an ELEVATED-goal variant (see +# ArenaBoundary.GoalMode) — read once _start() runs so _place_ball_near_goal +# can sample a height range matching the goal's real position. Read here +# rather than _ready(): TrainingMode has no _ready() override, and +# GameMode._ready() is what discovers `arena` before calling _start(). +var _elevated := false + func _start() -> void: + _elevated = (arena.get_node("Boundary") as ArenaBoundary).goal_mode == ArenaBoundary.GoalMode.ELEVATED _parse_eval_args() _parse_curriculum_args() spawn_ball() @@ -371,9 +379,12 @@ func _place_ball_random() -> void: func _place_ball_near_goal() -> void: var goal := _goal_for_team(0) if randf() < attack_goal_bias else _goal_for_team(1) var toward_centre := -signf(goal.global_position.z) + # Upper Y bound is a no-op on FLOOR arenas (goal.global_position.y ~0.79, + # so maxf(4.0, ...) stays 4.0); on ELEVATED arenas it widens to sample + # near the goal's real height instead of always landing near the floor. var position := Vector3( randf_range(-4.0, 4.0), - randf_range(FIELD_MIN_Y, 4.0), + randf_range(FIELD_MIN_Y, maxf(4.0, goal.global_position.y + 2.0)), goal.global_position.z + toward_centre * randf_range(3.0, 6.0) ) var to_goal := (goal.global_position - position).normalized()