OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / shapes / stairs_shape.gd

103 lines · gd ref:
extends BrushShape
class_name StairsShape
## Stairs, by TrenchBroom's rule (DrawShapeToolExtensions.cpp): tread depth is the run
## divided by the step count, and step i is a solid from the FLOOR up to its own tread —
## not a floating slab. That is what makes a flight watertight underneath and climbable
## rather than a set of hovering shelves.

const HEADROOM_M := 2.0        # the standing capsule is 1.58 m (player_procedural.tscn)


func id() -> String:
	return "stairs"


## STEPS 0 = AUTO: one step per grid step of height, which puts every tread on the lattice
## the map was built on (TrenchBroom's ceil(height / stepHeight) with stepHeight = the grid).
func params() -> Array:
	return [{"id": "steps", "label": "Steps", "type": "int", "default": 0, "min": 0, "max": 64,
		"step": 1, "tip": "How many steps. 0 = one per grid step of height."}]


## The step count the flight will have, auto resolved.
func step_count(lo: Vector3, hi: Vector3, ctx: Dictionary) -> int:
	var n := int(param(ctx, "steps"))
	if n > 0:
		return n
	var st: float = float(ctx.get("grid_step", 1.0))
	var vert: int = int(ctx["axis"])
	return maxi(1, roundi((hi[vert] - lo[vert]) / maxf(st, 1.0e-6)))


func pieces(lo: Vector3, hi: Vector3, ctx: Dictionary) -> Array:
	var vert: int = int(ctx["axis"])
	var run: int = int(ctx["run_axis"])
	var normal: Vector3 = ctx["normal"]
	var view: Vector3 = ctx["view"]
	var n: int = step_count(lo, hi, ctx)
	var step_h: float = (hi[vert] - lo[vert]) / float(n)
	var tread: float = (hi[run] - lo[run]) / float(n)
	# The flight rises AWAY FROM THE AIMED SURFACE, so stairs put on a floor go up.
	# On a ceiling the bounds hang downwards and so does the flight, which is what
	# the bounds say and is the same answer TrenchBroom gives.
	var up_positive: bool = normal[vert] >= 0.0
	# And it ASCENDS AWAY FROM YOU, so you arrive at the bottom step rather than
	# facing a wall of the top one.
	var away_positive: bool = view[run] > 0.0
	var out: Array = []
	for i in n:
		var slo: Vector3 = lo
		var shi: Vector3 = hi
		if up_positive:
			shi[vert] = lo[vert] + step_h * float(i + 1)
		else:
			slo[vert] = hi[vert] - step_h * float(i + 1)
		if away_positive:
			slo[run] = lo[run] + tread * float(i)
			shi[run] = lo[run] + tread * float(i + 1)
		else:
			slo[run] = hi[run] - tread * float(i + 1)
			shi[run] = hi[run] - tread * float(i)
		out.append(ShapeKit.box_piece(slo, shi))
	return out


## THE STAIRS CUT IS ONE BOX — the flight's own footprint, from the floor it
## stands on to the top of the flight plus headroom.
##
## Not the stepped complement of the treads, which was the other candidate. The
## flight is solid from the floor up to each tread (see pieces), so the
## complement is what is left of the bounds above them — a stair-shaped slot that
## hugs the geometry. It looks better and it is wrong for the same reason it
## looks better: it hugs. There is no headroom in it, so the ceiling of the cut
## IS the tread above, and you cannot walk up a flight whose ceiling is the next
## step. The rectangular cutting reads as a cutting because that is what it is.
func clearance(lo: Vector3, hi: Vector3, ctx: Dictionary) -> Array:
	var vert: int = int(ctx["axis"])
	var normal: Vector3 = ctx["normal"]
	var head: float = ShapeKit.lattice(HEADROOM_M, float(ctx["grid_step"]))
	var clo: Vector3 = lo
	var chi: Vector3 = hi
	# AWAY FROM THE AIMED SURFACE, the same direction the flight rises. On a
	# ceiling the bounds hang downwards and so does the headroom.
	if normal[vert] >= 0.0:
		chi[vert] = hi[vert] + head
	else:
		clo[vert] = lo[vert] - head
	return [ShapeKit.box_piece(clo, chi)]


func describe(lo: Vector3, hi: Vector3, ctx: Dictionary, size_text: String) -> String:
	var n: int = step_count(lo, hi, ctx)
	var vert: int = int(ctx["axis"])
	var st: float = maxf(float(ctx["grid_step"]), 1.0e-6)
	var rise_u: float = (hi[vert] - lo[vert]) / st * float(ctx["grid_units"]) / float(n)
	var fmt: Callable = ctx["fmt"]
	return "stairs, %d steps of %s in %s" % [n, String(fmt.call(rise_u)), size_text]


func clearance_name(ctx: Dictionary) -> String:
	var st: float = maxf(float(ctx["grid_step"]), 1.0e-6)
	var u: float = ShapeKit.lattice(HEADROOM_M, st) / st * float(ctx["grid_units"])
	return "%s units of headroom" % String.num(u, 0)