extends Node class_name Gauntlet ## THE GAUNTLET — destroy every file on one client, before the clock runs out. ## ## A wall of identical cabinets. The brief names a CLIENT, never a cabinet. Every cabinet ## carries an alphabetical range on its front, so working out that HENDERSON lives in F–J ## is the whole puzzle — there is no waypoint and nothing glows. ## ## Then it's granular on purpose: pull each drawer, wait for it to slide, and destroy the ## folders individually. Doing that with bare hands inside the time limit is not really ## possible. ## ## THE PERFECT TOOL. Every level hides one tool that makes its gauntlet tractable — here ## it's the SHREDDER, which eats a whole open drawer in one go. The game never tells you ## that. Noticing the shredder in the corner and realising what it's for IS the level. ## Getting it wrong isn't fatal, just slow, which is the right shape for a secret: it ## rewards the observant without punishing everyone else with a hard fail. ## ## Godot 4.7 GDScript 2.0. const RANGES := ["A–E", "F–J", "K–O", "P–T", "U–Z"] const CLIENTS := { "A–E": ["ABERNATHY", "CULVER", "DUNMORE", "BRATTON"], "F–J": ["HENDERSON", "GRIEVES", "IRONSIDE", "JAKUB"], "K–O": ["KOWALSKI", "MERRIWEATHER", "NAKAMURA", "OSGOOD"], "P–T": ["PENHALIGON", "QUAYLE", "SOMMERS", "TRENT"], "U–Z": ["VANCE", "WHITTAKER", "YARDLEY", "ZIEGLER"], } ## Generous: you stand ~2.2 m back to read the labels, your eye is 1.6 m up and the ## bottom drawer is at 0.10 m, so the true distance to a drawer is well over 2 m even ## when you're clearly standing at it. const REACH := 2.9 const SHRED_REACH := 1.9 @export var time_limit: float = 180.0 var running := false var time_left := 0.0 var result := "" var client := "" var target_range := "" var _cabs: Array[Cabinet] = [] var _target: Cabinet = null var _shredder: Node3D = null var _player: Node3D var _hud: Hud var _found_shredder := false signal finished() func setup(player: Node3D, hud: Hud) -> void: _player = player _hud = hud ## Build the bank of cabinets. `wire` hooks each Smashable into Main's signal routing. func begin(host: Node3D, origin: Vector3, yaw: float, wire: Callable, shredder: Node3D) -> void: for c in _cabs: if is_instance_valid(c): c.queue_free() _cabs.clear() _shredder = shredder _found_shredder = false var ranges := RANGES.duplicate() var basis := Basis(Vector3.UP, yaw) for i in ranges.size(): var cab := Cabinet.new() var off := basis * Vector3((float(i) - 2.0) * 0.62, 0.0, 0.0) cab.setup(host, origin + off, yaw, String(ranges[i]), wire) _cabs.append(cab) _target = _cabs[randi() % _cabs.size()] _target.is_target = true target_range = _target.label_range var pool: Array = CLIENTS[target_range] client = String(pool[randi() % pool.size()]) running = true time_left = time_limit result = "" if _hud != null: _hud.toast("DESTROY THE %s FILE — ALL OF IT" % client) ## Files left on the target client. func remaining() -> int: return _target.files_remaining() if is_instance_valid(_target) else 0 func total() -> int: return _target.files_total() if is_instance_valid(_target) else 0 # ---------------------------------------------------------------- interaction ## Player pressed E. Opens the nearest closed drawer, or feeds an open drawer to the ## shredder if you're standing at it. Returns true if it did something. func interact() -> bool: if not running or _player == null: return false var here := _player.global_position # at the shredder with a drawer already open? feed it the lot. if _shredder != null and is_instance_valid(_shredder): if here.distance_to(_shredder.global_position) < SHRED_REACH: return _shred() var best_cab: Cabinet = null var best_i := -1 var best_d := INF for c in _cabs: if not is_instance_valid(c): continue var i := c.nearest_closed(here, REACH) if i < 0: continue var d: float = c.drawers[i].global_position.distance_to(here) if d < best_d: best_d = d best_cab = c best_i = i if best_cab == null: return false best_cab.open(best_i) return true ## The shredder eats every folder in every OPEN drawer within reach of it. This is the ## payoff for working out what the shredder is for. func _shred() -> bool: var ate := 0 for c in _cabs: if not is_instance_valid(c): continue for i in Cabinet.DRAWERS: if not c.is_open(i): continue for f in c.drawer_files(i): if is_instance_valid(f): (f as Smashable).shatter(Vector3.UP * 0.5) ate += 1 if ate == 0: if _hud != null: _hud.toast("nothing to shred — open a drawer and bring it over") return false if not _found_shredder: _found_shredder = true if _hud != null: _hud.toast("…oh. THAT'S what that's for.") return true ## True when the player is close enough to the shredder for E to mean "shred". func at_shredder() -> bool: if _shredder == null or not is_instance_valid(_shredder) or _player == null: return false return _player.global_position.distance_to(_shredder.global_position) < SHRED_REACH # ---------------------------------------------------------------- clock func _process(dt: float) -> void: if not running: return if remaining() <= 0: _end(true) return time_left -= dt if time_left <= 0.0: time_left = 0.0 _end(false) func _end(won: bool) -> void: running = false if won: result = "%s IS GONE — %ds to spare" % [client, int(time_left)] else: result = "TIME. %s still has %d files. it was the %s drawer." % [ client, remaining(), target_range] if _hud != null: _hud.toast(result) finished.emit() func hud_line() -> String: if not running: return "GAUNTLET — %s (G restart · M mode)" % result var m := int(time_left) / 60 var s := int(time_left) % 60 return "GAUNTLET %d:%02d · destroy every %s file · %d/%d left%s" % [ m, s, client, remaining(), total(), " · E: SHRED" if at_shredder() else ""]