mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 00:14:00 +00:00
feat: add Nebula and Asteroid Field arenas
Introduce arena_02 (Nebula) and arena_03 (Asteroid Field) alongside arena_01, listed in a new ArenaRegistry (scripts/arena_registry.gd) as the single source of truth for available arenas. Free Play lets the player pick an arena from the main menu; Match/Spectate each pick one at random per session; Training keeps its own fixed arena_01. Nebula gets an RL-style visual treatment: a near-invisible glass boundary material shared by all arenas, a baked equirect nebula sky texture, a drifting-dust particle system, and a Blender-modeled station/debris/planet decoration set. GameMode gains a _get_arena_scene_path() hook so modes can instantiate their arena in code instead of hardcoding it in the scene.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
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.
|
||||
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"},
|
||||
]
|
||||
|
||||
|
||||
static func random_path() -> String:
|
||||
return ARENAS[randi() % ARENAS.size()]["path"]
|
||||
@@ -0,0 +1 @@
|
||||
uid://orkh5g5828cv
|
||||
@@ -4,6 +4,12 @@ extends GameMode
|
||||
# Rocket League's free play. R resets the ball, Esc returns to the menu.
|
||||
|
||||
|
||||
func _get_arena_scene_path() -> String:
|
||||
if not GameSettings.selected_arena_path.is_empty():
|
||||
return GameSettings.selected_arena_path
|
||||
return ArenaRegistry.ARENAS[0]["path"]
|
||||
|
||||
|
||||
func _start() -> void:
|
||||
spawn_ball()
|
||||
var player_ship := spawn_ship(0, 0, PlayerShipController.new())
|
||||
|
||||
@@ -26,6 +26,11 @@ func _ready():
|
||||
if child is Arena:
|
||||
arena = child
|
||||
break
|
||||
if not arena:
|
||||
var path := _get_arena_scene_path()
|
||||
if not path.is_empty():
|
||||
arena = (load(path) as PackedScene).instantiate()
|
||||
add_child(arena)
|
||||
if not arena:
|
||||
push_error("GameMode: scene has no Arena child")
|
||||
return
|
||||
@@ -34,6 +39,13 @@ func _ready():
|
||||
_start()
|
||||
|
||||
|
||||
# Virtual: subclasses whose scene has no fixed Arena child override this to
|
||||
# pick which arena scene to instantiate in code (see ArenaRegistry). Modes
|
||||
# that keep a fixed Arena child (training.tscn) never call this.
|
||||
func _get_arena_scene_path() -> String:
|
||||
return ""
|
||||
|
||||
|
||||
# Virtual: subclasses spawn their ball/ships/camera here.
|
||||
func _start() -> void:
|
||||
pass
|
||||
|
||||
@@ -21,3 +21,7 @@ var dev_bot_override_path: String = ""
|
||||
# Spectate (bot vs bot) policies chosen in the main menu; empty = mode export.
|
||||
var spectate_bot_a_path: String = ""
|
||||
var spectate_bot_b_path: String = ""
|
||||
|
||||
# Arena chosen in the main menu for Free Play; empty = ArenaRegistry's default.
|
||||
# Match/Spectate ignore this and pick a random arena each session instead.
|
||||
var selected_arena_path: String = ""
|
||||
|
||||
@@ -18,6 +18,7 @@ const DIFFICULTIES := [
|
||||
]
|
||||
|
||||
@onready var difficulty_dropdown: OptionButton = %DifficultyDropdown
|
||||
@onready var arena_dropdown: OptionButton = %ArenaDropdown
|
||||
@onready var dev_section: Control = %DevSection
|
||||
@onready var dev_bot_dropdown: OptionButton = %DevBotDropdown
|
||||
@onready var bot_a_dropdown: OptionButton = %BotADropdown
|
||||
@@ -26,6 +27,7 @@ const DIFFICULTIES := [
|
||||
|
||||
func _ready() -> void:
|
||||
_populate_difficulty_dropdown()
|
||||
_populate_arena_dropdown()
|
||||
dev_section.visible = OS.is_debug_build()
|
||||
if dev_section.visible:
|
||||
var bots := _list_bots()
|
||||
@@ -47,6 +49,19 @@ func _populate_difficulty_dropdown() -> void:
|
||||
difficulty_dropdown.select(selected)
|
||||
|
||||
|
||||
# Free Play only — Match/Spectate pick a random arena in code instead.
|
||||
func _populate_arena_dropdown() -> void:
|
||||
arena_dropdown.clear()
|
||||
for tier in ArenaRegistry.ARENAS:
|
||||
arena_dropdown.add_item(tier["name"])
|
||||
var selected := 0
|
||||
for i in ArenaRegistry.ARENAS.size():
|
||||
if ArenaRegistry.ARENAS[i]["path"] == GameSettings.selected_arena_path:
|
||||
selected = i
|
||||
break
|
||||
arena_dropdown.select(selected)
|
||||
|
||||
|
||||
# Lists bot checkpoints as paths relative to BOTS_DIR, including the
|
||||
# top-level curriculum checkpoints and anything under promoted/.
|
||||
func _list_bots() -> Array[String]:
|
||||
@@ -93,6 +108,8 @@ func _selected_path(dropdown: OptionButton) -> String:
|
||||
|
||||
|
||||
func _on_free_play_pressed() -> void:
|
||||
var chosen: Dictionary = ArenaRegistry.ARENAS[0] if arena_dropdown.selected < 0 else ArenaRegistry.ARENAS[arena_dropdown.selected]
|
||||
GameSettings.selected_arena_path = chosen["path"]
|
||||
get_tree().change_scene_to_file("res://scenes/free_play.tscn")
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ var match_timer: Timer
|
||||
var _in_overtime := false
|
||||
|
||||
|
||||
func _get_arena_scene_path() -> String:
|
||||
return ArenaRegistry.random_path()
|
||||
|
||||
|
||||
func _start() -> void:
|
||||
spawn_ball()
|
||||
var player_ship := spawn_ship(0, 0, PlayerShipController.new())
|
||||
|
||||
@@ -19,6 +19,10 @@ signal score_changed(score: Dictionary)
|
||||
var score := {0: 0, 1: 0}
|
||||
|
||||
|
||||
func _get_arena_scene_path() -> String:
|
||||
return ArenaRegistry.random_path()
|
||||
|
||||
|
||||
func _start() -> void:
|
||||
spawn_ball()
|
||||
# The main menu's dropdown selections (GameSettings autoload) win over the
|
||||
|
||||
Reference in New Issue
Block a user