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: # THE GREENGROCER: the stacks come down, the scale swings, an apple gets thrown at # the bottles, and the bag does not open. _script = [ {"dur": 1.2, "move_to": null, "look_at": null, "act": "level:grocer"}, {"dur": 0.3, "move_to": null, "look_at": null, "act": "mode:2"}, {"dur": 2.6, "move_to": Vector3(-2.0, 0, 4.4), "look_at": Vector3(-3.6, 1.3, 0.0), "act": ""}, {"dur": 2.0, "move_to": Vector3(-4.6, 0, 0.4), "look_at": Vector3(-5.4, 1.15, 1.4), "act": ""}, {"dur": 0.4, "move_to": null, "look_at": null, "act": "weapon:2"}, {"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"}, {"dur": 1.2, "move_to": null, "look_at": null, "act": "swing"}, {"dur": 1.4, "move_to": null, "look_at": null, "act": "swing"}, {"dur": 2.4, "move_to": Vector3(-4.9, 0, -1.4), "look_at": Vector3(-5.4, 1.10, -2.2), "act": ""}, {"dur": 1.2, "move_to": null, "look_at": null, "act": "swing"}, {"dur": 1.6, "move_to": null, "look_at": null, "act": "swing"}, # the hanging scale, which is at face height and stays swinging {"dur": 2.4, "move_to": Vector3(-3.7, 0, 0.9), "look_at": Vector3(-3.7, 1.78, -0.4), "act": ""}, {"dur": 1.4, "move_to": null, "look_at": null, "act": "swing"}, {"dur": 2.6, "move_to": null, "look_at": null, "act": ""}, # pick an orange up off the pile and throw it at the bottle shelf {"dur": 2.2, "move_to": Vector3(-5.4, 0, 2.5), "look_at": Vector3(-5.4, 1.10, 1.4), "act": ""}, {"dur": 0.6, "move_to": null, "look_at": null, "act": "grab"}, {"dur": 1.0, "move_to": null, "look_at": Vector3(-4.4, 1.70, -7.7), "act": ""}, {"dur": 0.4, "move_to": null, "look_at": null, "act": "throw"}, {"dur": 1.8, "move_to": null, "look_at": null, "act": ""}, # and the bottles, properly {"dur": 2.4, "move_to": Vector3(-3.0, 0, -6.4), "look_at": Vector3(-3.0, 1.75, -7.7), "act": ""}, {"dur": 0.4, "move_to": null, "look_at": null, "act": "weapon:4"}, {"dur": 1.4, "move_to": null, "look_at": null, "act": "swing"}, {"dur": 1.6, "move_to": null, "look_at": null, "act": "swing"}, # the bag. Stations only exist while you're on the clock, so clock back on. {"dur": 0.3, "move_to": null, "look_at": null, "act": "mode:0"}, {"dur": 2.8, "move_to": Vector3(-0.3, 0, 2.2), "look_at": Vector3(-0.3, 1.06, 3.6), "act": ""}, {"dur": 0.8, "move_to": null, "look_at": null, "act": "task"}, {"dur": 1.0, "move_to": null, "look_at": null, "act": "key:space"}, # rub away at it. The meter climbs the whole time you're getting nowhere. {"dur": 0.6, "move_to": null, "look_at": null, "act": "key:left"}, {"dur": 0.6, "move_to": null, "look_at": null, "act": "key:right"}, {"dur": 0.6, "move_to": null, "look_at": null, "act": "key:left"}, {"dur": 0.6, "move_to": null, "look_at": null, "act": "key:right"}, {"dur": 0.5, "move_to": null, "look_at": null, "act": "rage:0.16"}, {"dur": 0.6, "move_to": null, "look_at": null, "act": "key:left"}, {"dur": 0.6, "move_to": null, "look_at": null, "act": "key:right"}, {"dur": 0.5, "move_to": null, "look_at": null, "act": "rage:0.16"}, # turn it over. This was the answer the whole time. {"dur": 1.0, "move_to": null, "look_at": null, "act": "key:space"}, {"dur": 0.6, "move_to": null, "look_at": null, "act": "key:left"}, {"dur": 0.6, "move_to": null, "look_at": null, "act": "key:right"}, {"dur": 0.6, "move_to": null, "look_at": null, "act": "key:left"}, {"dur": 1.0, "move_to": null, "look_at": null, "act": "key:right"}, {"dur": 3.4, "move_to": Vector3(-1.0, 0, 2.0), "look_at": Vector3(-4.0, 0.6, -0.5), "act": ""}, ] 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("level:"): if _main.has_method("_load_level"): _main._load_level(act.substr(6)) return if act.begins_with("mode:"): var md = _main.get("_modes") if md != null: md.set_mode(int(act.substr(5))) return if act.begins_with("ent:"): # park the entity somewhere specific so the tour can look at it on cue var br = _main.get("_backrooms") if br != null and br.entity != null: var p := act.substr(4).split(",") br.entity.global_position = Vector3(float(p[0]), 0.0, float(p[1])) br.entity.state = Entity.S.STALKING return if act.begins_with("two:"): var b4 = _main.get("_backrooms") if b4 != null and b4.other != null: var q := act.substr(4).split(",") b4.other.wake() b4.other.global_position = Vector3(float(q[0]), 0.14, float(q[1])) b4.other.state = Entity.S.STALKING return if act == "kill": var b3 = _main.get("_backrooms") if b3 != null: b3.dread = 0.99 return if act == "scare": var br2 = _main.get("_backrooms") if br2 != null and br2.entity != null: br2.entity._cd = 0.0 br2.entity._catch() return if act == "puff": var dd = _main.get("_dust") if dd != null: dd._cd = 0.0 dd.discharge() return if act == "interact": var gg = _main.get("_gauntlet") if gg != null: gg.interact() 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 if act.begins_with("key:"): # feed one key to the open task panel var tk = _main.get("_tasks") var codes := {"space": KEY_SPACE, "left": KEY_LEFT, "right": KEY_RIGHT, "up": KEY_UP, "down": KEY_DOWN, "enter": KEY_ENTER} if tk != null: tk.handle_key(int(codes.get(act.substr(4), KEY_SPACE))) return match act: "grab": # E on whatever is under the crosshair if _player.grab != null: _player.grab.try_grab() "throw": # LMB while holding = chuck it if _player.grab != null: _player.grab.primary() "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()