OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / shapes / param / param_stairs.gd

118 lines · gd ref:
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))]