extends "res://scenes/maps/shapes/param/param_shape.gd"
## STAIRS (parametric). Local: x = the run, y = up, z = width; size = (length, height,
## width). Flights with landings, straight or U (switchback). L turns are a later step.
##
## A step is solid from the floor up to its tread (tread_thickness 0, TrenchBroom's
## rule, watertight and climbable) or a slab of tread_thickness. `gap` opens space
## between treads along the run (open risers).
## The riser auto steps aim for (steps = 0). 0.2 m is a comfortable domestic riser
## and well under the player's automatic step-up (docs/player-step-up-design.md).
const AUTO_RISER_M := 0.2
func id() -> String:
return "stairs"
func label() -> String:
return "Stairs"
func params() -> Array:
return [
{"id": "steps", "label": "Steps per flight", "type": "int", "default": 0, "min": 0, "max": 128,
"step": 1, "tip": "0 = auto: risers of about Auto riser."},
# §13.5a (user 2026-10-12: "configurable, both"): the riser auto aims for. The
# create tool sets it to one grid step for "one step per grid step". Default =
# AUTO_RISER_M, so records without it generate exactly what they did (gv 1).
{"id": "auto_riser", "label": "Auto riser", "type": "float", "default": AUTO_RISER_M,
"min": 0.01, "step": 0.01, "unit": "m", "tip": "The riser height Steps 0 aims for."},
{"id": "tread_thickness", "label": "Tread thickness", "type": "float", "default": 0.0,
"min": 0.0, "max": 2.0, "step": 0.05, "unit": "m", "tip": "0 = solid to the floor."},
{"id": "gap", "label": "Gap between treads", "type": "float", "default": 0.0,
"min": 0.0, "max": 1.0, "step": 0.05, "unit": "m", "tip": "Open space along the run."},
{"id": "flights", "label": "Flights", "type": "int", "default": 1, "min": 1, "max": 8, "step": 1},
{"id": "landing_depth", "label": "Landing depth", "type": "float", "default": 1.0,
"min": 0.3, "max": 10.0, "step": 0.1, "unit": "m"},
{"id": "turn", "label": "Turn", "type": "enum", "default": "straight",
"options": ["straight", "u"], "tip": "Between flights: straight on, or U (switchback)."},
{"id": "headroom", "label": "Headroom", "type": "float", "default": 2.0,
"min": 0.0, "max": 6.0, "step": 0.1, "unit": "m", "tip": "Clear space cut above the flight."},
]
func _steps(h_flight: float, p: Dictionary) -> int:
var n := int(p["steps"])
return n if n > 0 else maxi(1, roundi(h_flight / float(p["auto_riser"])))
## A step's or landing's vertical extent: up to `top`, down to the floor or a slab.
func _y(top: float, p: Dictionary) -> Vector2:
var tt: float = p["tread_thickness"]
return Vector2(maxf(top - tt, 0.0) if tt > 0.0 else 0.0, top)
func local_pieces(size: Vector3, p: Dictionary) -> Array:
var L := size.x
var H := size.y
var W := size.z
var F: int = p["flights"]
var hf := H / float(F)
var n := _steps(hf, p)
var riser := hf / float(n)
var out: Array = []
if String(p["turn"]) == "u" and F > 1:
# Lanes of half the width; flights in x in [D, L-D], alternating direction;
# landings full width at the end each flight arrives at.
var D: float = minf(float(p["landing_depth"]), L / 3.0)
var run := L - 2.0 * D
var tread := run / float(n)
var gap: float = minf(float(p["gap"]), tread * 0.9)
for f in F:
var forward := f % 2 == 0
var z0 := 0.0 if forward else W * 0.5
for i in n:
var y := _y(float(f) * hf + float(i + 1) * riser, p)
var xa: float
var xb: float
if forward:
xa = D + float(i) * tread
xb = xa + tread - gap
else:
xb = L - D - float(i) * tread
xa = xb - (tread - gap)
out.append(tagged(box(Vector3(xa, y.x, z0), Vector3(xb, y.y, z0 + W * 0.5)),
"step %d" % (f * n + i + 1)))
if f < F - 1:
var ly := _y(float(f + 1) * hf, p)
var lx0 := L - D if forward else 0.0
out.append(tagged(box(Vector3(lx0, ly.x, 0.0), Vector3(lx0 + D, ly.y, W)), "landing %d" % (f + 1)))
return out
# Straight: flights one after another along x, a landing between each.
var D2: float = minf(float(p["landing_depth"]), L / float(F) * 0.5) if F > 1 else 0.0
var run_f := (L - float(F - 1) * D2) / float(F)
var tread2 := run_f / float(n)
var gap2: float = minf(float(p["gap"]), tread2 * 0.9)
for f in F:
var x0 := float(f) * (run_f + D2)
for i in n:
var y2 := _y(float(f) * hf + float(i + 1) * riser, p)
var xa2 := x0 + float(i) * tread2
out.append(tagged(box(Vector3(xa2, y2.x, 0.0), Vector3(xa2 + tread2 - gap2, y2.y, W)),
"step %d" % (f * n + i + 1)))
if f < F - 1:
var ly2 := _y(float(f + 1) * hf, p)
out.append(tagged(box(Vector3(x0 + run_f, ly2.x, 0.0), Vector3(x0 + run_f + D2, ly2.y, W)),
"landing %d" % (f + 1)))
return out
## One box: the footprint, floor to the top of the flight plus headroom (see
## StairsShape.clearance for why it is a box and not the stepped complement).
func local_clearance(size: Vector3, p: Dictionary) -> Array:
var head: float = p["headroom"]
if head <= 0.0:
return []
return [box(Vector3.ZERO, Vector3(size.x, size.y + head, size.z))]