The game had no room. Props sat on a grey disc in a black void, which quietly broke the premise: a game about wrecking your workplace needs a workplace. Office.gd — LEVEL 01 - Open-plan office laid out after The Office (US) floor plan: bullpen of facing desk pairs behind cubicle partitions, reception, glass-walled manager's office, conference room, break room with vending machines, copier alcove, warehouse roller door. - Drop ceiling on a T-bar grid with fluorescent troffers that ARE the light sources (the old scene lit an interior with one outdoor directional lamp, which is exactly why it read as "props on a plane"), window wall with blinds, magnolia and carpet. - Office owns the shell + static fittings; Main._populate() owns everything smashable and asks Office where things go. Walls/desks/counters are boxes because they ARE boxes; chairs, vending machines, microwave and plant come from a new Blender generator (tools/gen_office_props.py) because a box reads as wrong for those. The level no longer falls over on its own - Records were stacked 3 cm apart vertically while being 30 cm TALL, so Jolt resolved 27 cm of interpenetration explosively on frame one and shoved the furniture over before the player touched anything. They now stand side by side. - Per-material mass (MASSES): everything was 1 kg, so a thrown record could tip a filing cabinet. - Spawn guard: placing ~40 props by formula guarantees an occasional overlap, and depenetration is violent (a chair left the building at 500 m/s). Dynamic bodies are speed-limited for 0.75 s, and each offender is named once with the position it was PLACED at, so the cause stays visible instead of being papered over. It then found the real bug: bullpen rows 3.6 m apart left the two rows' chairs meeting back-to-back with 3 cm to spare. Rows are now 4.8 m apart. - dev/DemoDriver.gd act 0 touches nothing for 5 s and prints total body speed. Now reads 0.00 m/s with zero spawn warnings. Two real melee bugs found while testing the level - The hit test was a SPHERE parked at `reach`, so anything CLOSER than the weapon's reach fell in front of it and was missed — you could stand against the printer with a sledgehammer and swing straight through it. Now a capsule swept from the camera. - Swings started at eye height (1.6 m), so a carton on the floor was ~1.5 m away even standing over it and short-reach weapons could never touch anything on the ground. Swings now originate at hand height. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
212 lines
8.0 KiB
GDScript
212 lines
8.0 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 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
|
|
match act:
|
|
"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()
|