Files
Papay-Shooter/scenes/maps/procedural_arena/arena_generator.gd
Nicholas ButzkeandClaude Fable 5 198345177a feat: world-grid level materials + real movement test suite
- Generated CC0-style prototype grid textures (0.5 m cells) and a shared
  LevelMaterials factory: world-space triplanar mapping over every code-built
  level (test level, dust2, procedural arena) with the existing color coding
  kept as tints. Readable surfaces at speed instead of flat color boxes;
  materials are cached per tint so identical surfaces batch.
- .gdignore + .gitignore the raw downloaded asset packs in addons/ whose
  overlong paths broke Godot import scans and spammed git warnings.
- Replaced the rotted FSM test file (invalid call(t) on Callables, stateless
  stub nodes, hung forever without quitting) with a real suite: 10 tests
  covering state registration, jump bookkeeping, wall-run momentum
  preservation and upward-carry cap, per-player dash cooldown (regression for
  the old shared static), dash speed stacking, chain soft cap, landing
  events, and slide entry momentum. New headless entrypoint:
  godot --headless --path . -s res://movement/tests/run_fsm_tests.gd

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-17 12:14:30 -04:00

216 lines
7.2 KiB
GDScript

@tool
extends Node3D
class_name ArenaGenerator
@export var generate: bool = false:
set(value):
if value:
if not is_inside_tree():
call_deferred("_generate_tactical_arena")
else:
_generate_tactical_arena()
generate = false
@export var clear: bool = false:
set(value):
if value:
_clear_arena()
clear = false
@export var sector_size = 24.0
@export var step_height = 3.0
@export var wall_height = 12.0
@export var floor_color = Color(0.2, 0.22, 0.25)
@export var wall_color = Color(0.3, 0.4, 0.5)
@export var obstacle_color = Color(0.6, 0.35, 0.15)
@export var ramp_color = Color(0.7, 0.4, 0.2)
func _clear_arena() -> void:
var children = get_children()
for c in children:
remove_child(c)
c.free()
func _generate_tactical_arena() -> void:
_clear_arena()
var cols = 3 # Left, Mid, Right
var rows = 5 # Spawn A, Conn A, Mid, Conn B, Spawn B
# Generate elevations (symmetric for fairness)
var elevations = []
for x in range(cols):
elevations.append([])
for z in range(rows):
elevations[x].append(0)
# Randomize half, mirror other half
for x in range(cols):
for z in range(3): # 0, 1, 2
elevations[x][z] = randi_range(0, 2)
# Mirror to the other side (point symmetry)
# Or mirror horizontally (Spawn A == Spawn B setup)
# A standard mirror across mid:
for x in range(cols):
elevations[x][3] = elevations[cols - 1 - x][1]
elevations[x][4] = elevations[cols - 1 - x][0]
# Build Sector Floors
var _sec_size = float(sector_size if sector_size != null else 24.0)
var _step_h = float(step_height if step_height != null else 3.0)
var _wall_h = float(wall_height if wall_height != null else 12.0)
var _f_col = floor_color if floor_color != null else Color(0.2, 0.22, 0.25)
var _w_col = wall_color if wall_color != null else Color(0.3, 0.4, 0.5)
var hw = (cols * _sec_size) / 2.0
var hl = (rows * _sec_size) / 2.0
# Global origin is center of grid
for x in range(cols):
for z in range(rows):
var cx = -hw + x * _sec_size + _sec_size / 2.0
var cz = -hl + z * _sec_size + _sec_size / 2.0
var y = float(elevations[x][z]) * _step_h
# Make floors incredibly thick (40 units) so they act as solid pillars all the way down
# This prevents players from walking underneath higher sectors and falling into the void
_create_box("Sector_%d_%d" % [x, z], Vector3(cx, y - 20.0, cz), Vector3(_sec_size, 40.0, _sec_size), _f_col)
# Add cover in sector (except spawns z=0 and z=4)
if z > 0 and z < 4:
_generate_cover("Cover_%d_%d" % [x, z], Vector3(cx, y, cz))
# Ramps between Z adjacent (North/South)
if z < rows - 1:
var diff = elevations[x][z+1] - elevations[x][z]
if diff != 0:
_create_ramp_z("RampZ_%d_%d" % [x, z], cx, cz + _sec_size / 2.0, y, diff)
# Ramps/Walls between X adjacent (East/West)
if x < cols - 1:
var is_connector = randf() > 0.4 # 60% chance to be an open connector
var diff = elevations[x+1][z] - elevations[x][z]
if is_connector:
if diff != 0:
_create_ramp_x("RampX_%d_%d" % [x, z], cx + _sec_size / 2.0, cz, y, diff)
else:
# Solid wall
var max_y = max(elevations[x][z], elevations[x+1][z]) * _step_h
_create_box("InnerWall_%d_%d" % [x, z], Vector3(cx + _sec_size / 2.0, max_y + _wall_h/2, cz), Vector3(1.0, _wall_h, _sec_size), _w_col)
# Outer Walls
_create_box("Wall_North", Vector3(0, _wall_h * 0.5, -hl), Vector3(hw * 2, _wall_h, 1.0), _w_col)
_create_box("Wall_South", Vector3(0, _wall_h * 0.5, hl), Vector3(hw * 2, _wall_h, 1.0), _w_col)
_create_box("Wall_East", Vector3(hw, _wall_h * 0.5, 0), Vector3(1.0, _wall_h, hl * 2), _w_col)
_create_box("Wall_West", Vector3(-hw, _wall_h * 0.5, 0), Vector3(1.0, _wall_h, hl * 2), _w_col)
func _generate_cover(base_name: String, center: Vector3) -> void:
var _sec_size = float(sector_size if sector_size != null else 24.0)
var _o_col = obstacle_color if obstacle_color != null else Color(0.6, 0.35, 0.15)
# Generate 1-3 blocks of cover
var count = randi_range(1, 3)
for i in range(count):
var ox = randf_range(-_sec_size*0.3, _sec_size*0.3)
var oz = randf_range(-_sec_size*0.3, _sec_size*0.3)
var sx = randf_range(2.0, 6.0)
var sy = randf_range(1.0, 4.0)
var sz = randf_range(2.0, 6.0)
_create_box("%s_%d" % [base_name, i], center + Vector3(ox, sy * 0.5, oz), Vector3(sx, sy, sz), _o_col)
func _create_ramp_z(node_name: String, cx: float, cz: float, base_y: float, diff_levels: int) -> void:
var _step_h = float(step_height if step_height != null else 3.0)
var _sec_size = float(sector_size if sector_size != null else 24.0)
var _r_col = ramp_color if ramp_color != null else Color(0.7, 0.4, 0.2)
# Ramp along Z axis (North/South)
var h = abs(diff_levels) * _step_h
var length = _sec_size * 0.4 # Ramp takes up 40% of sector depth
var center_y = base_y + h / 2.0
var dir = sign(diff_levels)
var rot_x = 0
if dir > 0:
rot_x = rad_to_deg(atan2(h, length))
else:
rot_x = -rad_to_deg(atan2(h, length))
var true_length = sqrt(length*length + h*h)
_create_ramp(node_name, Vector3(cx, center_y, cz), Vector3(_sec_size * 0.5, 0.5, true_length), Vector3(rot_x, 0, 0), _r_col)
func _create_ramp_x(node_name: String, cx: float, cz: float, base_y: float, diff_levels: int) -> void:
var _step_h = float(step_height if step_height != null else 3.0)
var _sec_size = float(sector_size if sector_size != null else 24.0)
var _r_col = ramp_color if ramp_color != null else Color(0.7, 0.4, 0.2)
# Ramp along X axis (East/West)
var h = abs(diff_levels) * _step_h
var length = _sec_size * 0.4
var center_y = base_y + h / 2.0
var dir = sign(diff_levels)
var rot_z = 0
if dir > 0:
rot_z = -rad_to_deg(atan2(h, length))
else:
rot_z = rad_to_deg(atan2(h, length))
var true_length = sqrt(length*length + h*h)
_create_ramp(node_name, Vector3(cx, center_y, cz), Vector3(true_length, 0.5, _sec_size * 0.5), Vector3(0, 0, rot_z), _r_col)
func _create_box(node_name: String, pos: Vector3, size: Vector3, color: Color) -> void:
var body := StaticBody3D.new()
body.name = node_name
body.position = pos
var shape := CollisionShape3D.new()
shape.name = "CollisionShape3D"
shape.shape = BoxShape3D.new()
shape.shape.size = size
body.add_child(shape)
var mesh := MeshInstance3D.new()
mesh.name = "MeshInstance3D"
mesh.mesh = BoxMesh.new()
mesh.mesh.size = size
mesh.mesh.surface_set_material(0, LevelMaterials.tinted(color))
body.add_child(mesh)
add_child(body)
if Engine.is_editor_hint():
var scene_root = get_tree().edited_scene_root
if scene_root:
body.owner = scene_root
shape.owner = scene_root
mesh.owner = scene_root
func _create_ramp(node_name: String, pos: Vector3, size: Vector3, rot_deg: Vector3, color: Color) -> void:
var body := StaticBody3D.new()
body.name = node_name
body.position = pos
body.rotation_degrees = rot_deg
var shape := CollisionShape3D.new()
shape.name = "CollisionShape3D"
shape.shape = BoxShape3D.new()
shape.shape.size = size
body.add_child(shape)
var mesh := MeshInstance3D.new()
mesh.name = "MeshInstance3D"
mesh.mesh = BoxMesh.new()
mesh.mesh.size = size
mesh.mesh.surface_set_material(0, LevelMaterials.tinted(color))
body.add_child(mesh)
add_child(body)
if Engine.is_editor_hint():
var scene_root = get_tree().edited_scene_root
if scene_root:
body.owner = scene_root
shape.owner = scene_root
mesh.owner = scene_root