Files
CosmicClash/Game/scripts/arena.gd
T
Josh Creek 04691aaa48 chore(multiplayer): Phase 0 refactors + graphics/perf settings groundwork
Lands the non-networked Phase 0 tasks from multiplayer-todo.md (ship/camera/
arena refactors, sim constants, background FPS handling) plus a first pass
at exposing graphics/performance settings (presets, resolution scaling,
vsync, FPS cap, perf overlay) and a GPU profiling harness for the
real-hardware follow-up in task 0.15b.
2026-08-19 22:37:17 +01:00

97 lines
3.7 KiB
GDScript

class_name Arena
extends Node3D
# A reusable stadium: terrain, lighting, environment, two team goals, and
# spawn markers. An arena holds no rules and no state — game modes query it
# for spawn transforms and goals, then spawn ships/ball themselves.
# Per-arena Environment tuning. These live as exports (rather than baked
# into each arena's Environment sub-resource) because Godot's scene
# inheritance can only override whole top-level node properties, not
# properties nested inside a sub-resource — so each arena_0N.tscn overrides
# these on the Arena root instead, and _ready() applies them here.
@export var sky_material: Material
@export var ambient_light_color := Color(0.55, 0.6, 0.75, 1)
@export var ambient_light_energy := 0.4
@export var glow_intensity := 0.9
@export var glow_strength := 1.2
@export var glow_bloom := 0.05
@export var glow_hdr_threshold := 1.0
var _env: Environment
# Lights authored with shadow_enabled = true (the DirectionalLight3D + 4
# PitchLights omnis) — captured once, before gating ever touches them. Every
# call after the first re-applies VideoSettings.shadows_enabled to exactly
# these lights, so the set can't self-poison (if it were re-derived from
# current state, a light this same code just turned off would look
# indistinguishable from FillLight, which is authored off on purpose and must
# never be turned on by the preset ladder).
var _shadow_capable_lights: Array[Light3D] = []
func _ready():
add_to_group("arena")
# A headless server never renders, so duplicating and configuring a full
# Environment (glow/SSAO/SSIL/SDFGI) for it is pure waste — mirrors the
# same guard at ship.gd and arena_boundary.gd.
if DisplayServer.get_name() == "headless":
return
var world_env := get_node_or_null("WorldEnvironment") as WorldEnvironment
if world_env and world_env.environment:
_env = world_env.environment.duplicate(true) as Environment
world_env.environment = _env
if sky_material:
if not _env.sky:
_env.sky = Sky.new()
_env.sky.sky_material = sky_material
_env.ambient_light_color = ambient_light_color
_env.ambient_light_energy = ambient_light_energy
_env.glow_intensity = glow_intensity
_env.glow_strength = glow_strength
_env.glow_bloom = glow_bloom
_env.glow_hdr_threshold = glow_hdr_threshold
for light in find_children("*", "Light3D", true, false):
if (light as Light3D).shadow_enabled:
_shadow_capable_lights.append(light)
_apply_video_settings()
# Task 0.17: a preset change from the settings menu must take effect
# on the arena that's already loaded, not just the next one — this is
# the "settings persist and apply without a restart" acceptance bar.
VideoSettings.settings_changed.connect(_apply_video_settings)
# Re-run on every VideoSettings.settings_changed (preset or individual
# toggle) as well as once at load. Shadow gating lives here rather than in
# VideoSettings.apply_to_environment() because it targets Light3D nodes in
# this arena's own tree, not the Environment resource.
func _apply_video_settings() -> void:
if not is_instance_valid(_env):
return
VideoSettings.apply_to_environment(_env)
for light in _shadow_capable_lights:
if is_instance_valid(light):
light.shadow_enabled = VideoSettings.shadows_enabled
func get_ball_spawn() -> Transform3D:
return $BallSpawn.global_transform
func get_ship_spawns(team: int) -> Array[Transform3D]:
var spawns: Array[Transform3D] = []
var container := get_node_or_null("SpawnsTeam%d" % team)
if container:
for child in container.get_children():
if child is Marker3D:
spawns.append(child.global_transform)
return spawns
func get_goals() -> Array[Goal]:
var goals: Array[Goal] = []
for child in get_children():
if child is Goal:
goals.append(child)
return goals