The game stopped being a smash sandbox. You work a shift, the work provokes you, a
meter fills, you snap, and you cannot go back to your desk until you have broken
enough things. Then you sit back down and try to earn the rest of your day.
The score is your payslip: WAGES (tasks completed) - DAMAGES (everything you broke).
The load-bearing detail is that rage drains a FLAT amount per object destroyed while
each object bills at its OWN value — so during a meltdown the play is to wreck a lot
of cheap rubbish fast rather than put the sledgehammer through a filing cabinet. You
don't choose whether you lose your temper, you choose how much it costs. FURY (objects
broken while melting down) is tracked separately and deliberately isn't money, so
"I ended the shift owing them $2,400" stays a brag.
Rage.gd — WORKING -> MELTDOWN -> COOLDOWN. During a meltdown the clock STALLS if you
stop destroying; you must actually be breaking things.
RageOverlay.gd — the blood vessel. A vignette that closes in with rage and jumps
inward on every heartbeat, procedural veins that creep further into frame the angrier
you get, and a synthesized lub-dub going 62 -> 172 BPM. Zero assets: the shader is
built in code, the veins are draw_polyline from a seeded RNG, the heartbeat is a
generated WAV. Tops out as a heavy frame rather than a red screen — you have to be
able to see what you're about to destroy.
Tasks.gd — stations you walk to and are LOCKED IN PLACE at, because you're at your
desk and that's the joke. Two so far:
SPREADSHEET the selected cell silently drifts one cell partway through entry, with
no sound and no animation. Commit without noticing and the figure lands
in the wrong cell and you start again. This is the whole thesis of the
game in about forty lines.
PHOTOCOPIER jams, and wants the right tray out of 1, 2, 2A, 3, 3B.
Snapping ejects you from the station at no penalty — you didn't choose to walk away.
CubicleHell.gd — 5-minute shift, wages vs damages, meltdown count, end-of-shift
payslip with a verdict line.
Also: HUD gets a monospace face (Godot's fallback is proportional, so the spreadsheet
grid and the work-order docket never lined up), and the arcade HUD's headline becomes
net pay, red when negative.
LANES/LANE7-cubicle-hell.md captures the rest of the design: more provocations, the
GAUNTLET mode (find which of many cabinets, pull drawers, destroy files individually,
and work out which tool the level wants without being told), the other three
workplaces, and why doratracyer/floor_plan can't help — it's OpenSCAD SVG->STL for
3D printing, no generation, no room polygons, GPL-3.0. Office.gd is already most of
a parametric floor-plan builder; lifting its numbers into a data spec is the real path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
251 lines
9.9 KiB
GDScript
251 lines
9.9 KiB
GDScript
extends Node3D
|
|
|
|
## Dev-only capture harness. Loads main.tscn and drives the player through a scripted
|
|
## tour so a headless/CI run can record video of the actual game with Godot's
|
|
## --write-movie, without a human at the keyboard (and without screen-recording perms).
|
|
##
|
|
## /Applications/Godot.app/Contents/MacOS/Godot --path game \
|
|
## --resolution 1280x720 --write-movie /tmp/cap.avi --quit-after 900 \
|
|
## dev/demo.tscn
|
|
##
|
|
## Not shipped: excluded from export presets. Drives the player directly (position /
|
|
## yaw / pitch / _queue_attack) rather than faking InputEvents, so it stays immune to
|
|
## input-map changes.
|
|
|
|
const MAIN := "res://main.tscn"
|
|
|
|
var _main: Node3D
|
|
var _player: Player
|
|
var _t := 0.0
|
|
var _step := 0
|
|
var _step_t := 0.0
|
|
var _frames := 0
|
|
|
|
# Each step: {dur, move_to (Vector3 or null), look_at (Vector3 or null), act (String)}
|
|
# act: "" | "punch" | "kick" | "rain" | "reset"
|
|
var _script: Array = []
|
|
|
|
func _ready() -> void:
|
|
var packed := load(MAIN) as PackedScene
|
|
_main = packed.instantiate()
|
|
add_child(_main)
|
|
await get_tree().process_frame
|
|
_player = _find_player(_main)
|
|
if _player == null:
|
|
push_error("[demo] no Player found")
|
|
return
|
|
# the demo drives the camera; don't let the OS grab the pointer
|
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
_build_script()
|
|
|
|
func _find_player(n: Node) -> Player:
|
|
if n is Player:
|
|
return n
|
|
for c in n.get_children():
|
|
var r := _find_player(c)
|
|
if r != null:
|
|
return r
|
|
return null
|
|
|
|
func _build_script() -> void:
|
|
# Office floor plan (see Office.gd): spawn at reception (-8.6, 6.6) looking into the
|
|
# bullpen; desk clusters at (-4.6,-1.4) (-4.6,2.2) (-0.4,-1.4) (-0.4,2.2); copier
|
|
# alcove at (3.0,-0.3); break room x>5.4 z>-1.2; conference and manager along z<-4.4;
|
|
# warehouse corner around (7,-6).
|
|
_script = []
|
|
|
|
# ---- ACT 0: SETTLE CHECK. Stand at the entrance and touch nothing. If anything
|
|
# ---- moves in these five seconds, the level is falling over by itself again.
|
|
_script += [
|
|
{"dur": 2.5, "move_to": Vector3(-8.6, 0, 6.4), "look_at": Vector3(-4.0, 1.1, 0.0), "act": ""},
|
|
{"dur": 2.5, "move_to": null, "look_at": null, "act": ""},
|
|
]
|
|
|
|
# ---- ACT 0b: CUBICLE HELL. Sit at a desk, do the spreadsheet, get wound up until
|
|
# ---- the meter snaps, then wreck the place until it drains.
|
|
_script += [
|
|
{"dur": 2.4, "move_to": Vector3(-4.6, 0, -0.55), "look_at": Vector3(-4.6, 1.0, -1.4), "act": ""},
|
|
{"dur": 1.2, "move_to": null, "look_at": null, "act": "task"},
|
|
{"dur": 1.6, "move_to": null, "look_at": null, "act": "type"},
|
|
{"dur": 1.6, "move_to": null, "look_at": null, "act": "commit"},
|
|
{"dur": 1.4, "move_to": null, "look_at": null, "act": "rage:0.30"},
|
|
{"dur": 1.4, "move_to": null, "look_at": null, "act": "rage:0.30"},
|
|
{"dur": 1.6, "move_to": null, "look_at": null, "act": "rage:0.35"},
|
|
# SNAP — the vignette closes in, veins crawl, heartbeat goes. Now wreck things.
|
|
{"dur": 1.0, "move_to": Vector3(-4.6, 0, 0.3), "look_at": Vector3(-4.6, 0.9, -0.7), "act": "weapon:2"},
|
|
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 1.6, "move_to": Vector3(-2.6, 0, 4.8), "look_at": Vector3(-2.3, 0.25, 5.2), "act": ""},
|
|
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 1.2, "move_to": null, "look_at": null, "act": "swing"},
|
|
]
|
|
|
|
# ---- ACT 1: a walk through the floor plan, so the rooms read.
|
|
_script += [
|
|
{"dur": 2.2, "move_to": Vector3(-8.4, 0, 2.0), "look_at": Vector3(-4.6, 1.1, 1.0), "act": ""},
|
|
{"dur": 2.2, "move_to": Vector3(-2.6, 0, 3.4), "look_at": Vector3(-0.4, 1.0, 0.5), "act": ""},
|
|
{"dur": 2.2, "move_to": Vector3(-3.0, 0, -3.0), "look_at": Vector3(-3.4, 1.4, -6.4), "act": ""},
|
|
{"dur": 2.2, "move_to": Vector3(1.6, 0, -2.0), "look_at": Vector3(8.0, 1.3, -6.5), "act": ""},
|
|
{"dur": 2.2, "move_to": Vector3(3.4, 0, 3.0), "look_at": Vector3(8.0, 1.2, 4.0), "act": ""},
|
|
]
|
|
|
|
# ---- ACT 2: the loadout, held still so each grip can be inspected.
|
|
for slot in range(6):
|
|
_script.append({"dur": 0.5, "move_to": Vector3(-2.2, 0, 4.6),
|
|
"look_at": Vector3(-2.0, 1.2, 0.0), "act": "weapon:%d" % slot})
|
|
_script.append({"dur": 0.8, "move_to": null, "look_at": null, "act": ""})
|
|
_script.append({"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"})
|
|
|
|
# ---- ACT 3: the matrix. Cutter shreds a carton, clanks off a filing cabinet,
|
|
# ---- then the sledge takes the cabinet out.
|
|
_script += [
|
|
{"dur": 0.5, "move_to": null, "look_at": null, "act": "weapon:1"},
|
|
{"dur": 2.0, "move_to": Vector3(-1.6, 0, 5.3), "look_at": Vector3(-2.3, 0.25, 5.2), "act": ""},
|
|
{"dur": 0.5, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 2.0, "move_to": Vector3(3.1, 0, -0.9), "look_at": Vector3(4.5, 0.6, -0.9), "act": ""},
|
|
{"dur": 0.4, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 0.5, "move_to": null, "look_at": null, "act": "weapon:4"},
|
|
{"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 1.3, "move_to": null, "look_at": null, "act": "swing"},
|
|
]
|
|
|
|
# ---- ACT 4: the printer boss, then a lap of the HUD styles.
|
|
_script += [
|
|
{"dur": 2.0, "move_to": Vector3(1.5, 0, -0.3), "look_at": Vector3(3.0, 0.95, -0.3), "act": ""},
|
|
{"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 1.5, "move_to": null, "look_at": null, "act": "swing"},
|
|
{"dur": 2.0, "move_to": Vector3(-1.0, 0, 5.6), "look_at": Vector3(-1.0, 1.2, -1.0), "act": "hud"},
|
|
{"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"},
|
|
{"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"},
|
|
{"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"},
|
|
]
|
|
|
|
## Total speed across every physics body in the level. During the settle act nobody has
|
|
## touched anything, so this MUST fall to ~0 — that is the objective version of "is the
|
|
## level quietly falling over on its own before the player does anything".
|
|
func _motion(verbose := false) -> float:
|
|
var total := 0.0
|
|
var seen := {}
|
|
var worst: Array = []
|
|
for g in ["smashable", "debris"]: # every body is in one of these two
|
|
for n in get_tree().get_nodes_in_group(g):
|
|
if seen.has(n) or not (n is RigidBody3D):
|
|
continue
|
|
seen[n] = true
|
|
var b := n as RigidBody3D
|
|
if b.freeze:
|
|
continue
|
|
var v := b.linear_velocity.length()
|
|
total += v
|
|
worst.append([v, b])
|
|
if verbose:
|
|
worst.sort_custom(func(a, c): return a[0] > c[0])
|
|
for i in mini(6, worst.size()):
|
|
var b: RigidBody3D = worst[i][1]
|
|
var what := "RigidBody3D"
|
|
if b is Record:
|
|
what = "Record(sleeve %s)" % (b as Record).sleeve_genre
|
|
elif b is Smashable:
|
|
what = "Smashable(%s)" % (b as Smashable).kind
|
|
var groups := ""
|
|
for g in b.get_groups():
|
|
groups += str(g) + " "
|
|
print(" %7.1f m/s %-18s %-22s parent=%s groups=[%s] at %s" % [
|
|
worst[i][0], b.name, what, b.get_parent().name, groups.strip_edges(),
|
|
b.global_position.snappedf(0.01)])
|
|
return total
|
|
|
|
func _process(dt: float) -> void:
|
|
if _player == null or _step >= _script.size():
|
|
return
|
|
if _frames < 3:
|
|
_frames += 1
|
|
if _frames == 2:
|
|
print("[settle] FIRST FRAME — where things start:")
|
|
_motion(true)
|
|
if _t < 6.0 and int(_t) != int(_t + dt):
|
|
print("[settle] t=%ds total body speed = %.2f m/s" % [int(_t + dt), _motion(true)])
|
|
_t += dt
|
|
var s: Dictionary = _script[_step]
|
|
var dur: float = s["dur"]
|
|
# fire the action when the step BEGINS, so a capture at step_start + n frames
|
|
# actually shows the swing rather than the second of stillness before it
|
|
if not s.get("_fired", false):
|
|
s["_fired"] = true
|
|
_do(String(s["act"]))
|
|
_step_t += dt
|
|
|
|
var mt = s.get("move_to")
|
|
if mt != null:
|
|
var to: Vector3 = mt
|
|
var k: float = clampf(_step_t / maxf(dur, 0.001), 0.0, 1.0)
|
|
k = k * k * (3.0 - 2.0 * k) # smoothstep so the dolly eases
|
|
var from: Vector3 = s.get("_from", _player.global_position)
|
|
if not s.has("_from"):
|
|
s["_from"] = _player.global_position
|
|
from = _player.global_position
|
|
_player.global_position = from.lerp(to, k)
|
|
var la = s.get("look_at")
|
|
if la != null:
|
|
_aim(la, clampf(_step_t / maxf(dur, 0.001), 0.0, 1.0))
|
|
|
|
if _step_t >= dur:
|
|
_step += 1
|
|
_step_t = 0.0
|
|
|
|
## Turn the body (yaw) + head (pitch) toward a world point, eased.
|
|
func _aim(target: Vector3, k: float) -> void:
|
|
var eye: Vector3 = _player.global_position + Vector3(0, _player.eye_height, 0)
|
|
var d: Vector3 = target - eye
|
|
if d.length() < 0.001:
|
|
return
|
|
var want_yaw := atan2(-d.x, -d.z)
|
|
var want_pitch := atan2(d.y, Vector2(d.x, d.z).length())
|
|
var e := clampf(k * 0.14, 0.0, 1.0) + 0.06
|
|
_player.rotation.y = lerp_angle(_player.rotation.y, want_yaw, e)
|
|
var head: Node3D = _player.get_node_or_null("Head")
|
|
if head != null:
|
|
head.rotation.x = lerp_angle(head.rotation.x, want_pitch, e)
|
|
|
|
func _do(act: String) -> void:
|
|
if act.begins_with("weapon:"):
|
|
_player.select(int(act.substr(7)))
|
|
return
|
|
if act.begins_with("rage:"):
|
|
var r = _main.get("_rage")
|
|
if r != null:
|
|
r.provoke(float(act.substr(5)), "the demo is provoking you")
|
|
return
|
|
match act:
|
|
"task": # press E at the nearest station
|
|
var t = _main.get("_tasks")
|
|
if t != null:
|
|
t.try_start()
|
|
"type": # enter the requested figure correctly
|
|
var t2 = _main.get("_tasks")
|
|
if t2 != null and t2.is_working():
|
|
var st: Dictionary = t2.panel_state()
|
|
var want := String(st.get("instruction", "")).split(" ")[1]
|
|
for ch in want:
|
|
t2.handle_key(KEY_0 + int(ch))
|
|
"commit":
|
|
var t3 = _main.get("_tasks")
|
|
if t3 != null:
|
|
t3.handle_key(KEY_ENTER)
|
|
"swing":
|
|
_player._queued = true # same path a left-click takes
|
|
"hud":
|
|
var hud = _main.get("_hud")
|
|
if hud != null:
|
|
hud.cycle()
|
|
"rain":
|
|
if _main.has_method("_rain"):
|
|
_main._rain(500)
|
|
"reset":
|
|
if _main.has_method("_reset"):
|
|
_main._reset()
|