From fe5a7d44223851e4266b8bd4e7d7cdadcafea599 Mon Sep 17 00:00:00 2001 From: Monster Robot Party Date: Tue, 4 Aug 2026 00:05:33 +1000 Subject: [PATCH] =?UTF-8?q?LANE9:=20the=20incident=20report=20=E2=80=94=20?= =?UTF-8?q?HR=20itemizes=20your=20meltdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a SNAP window ends (rage.calmed), scripts/Receipt.gd prints a dot-matrix incident report on green-bar paper, line by line with a chirp each: every material you broke DURING the meltdown as OBJECT / unit $ / xN, then TOTAL DAMAGES, then DEDUCTED FROM PAY. It's a reading of score data the payslip already tracks, made legible in the moment it costs you. Any key finishes the print then dismisses; five seconds does it on its own. A meltdown where you broke nothing prints nothing. dev/probe_receipt.gd: empty meltdown -> no receipt; glass x2 + paper -> $85 total. All gates green. Co-Authored-By: Claude Fable 5 --- game/dev/probe_receipt.gd | 57 ++++++++++++ game/dev/probe_receipt.gd.uid | 1 + game/dev/probe_roomba.gd.uid | 1 + game/scripts/Main.gd | 10 +++ game/scripts/Receipt.gd | 164 ++++++++++++++++++++++++++++++++++ game/scripts/Receipt.gd.uid | 1 + 6 files changed, 234 insertions(+) create mode 100644 game/dev/probe_receipt.gd create mode 100644 game/dev/probe_receipt.gd.uid create mode 100644 game/dev/probe_roomba.gd.uid create mode 100644 game/scripts/Receipt.gd create mode 100644 game/scripts/Receipt.gd.uid diff --git a/game/dev/probe_receipt.gd b/game/dev/probe_receipt.gd new file mode 100644 index 0000000..c8202af --- /dev/null +++ b/game/dev/probe_receipt.gd @@ -0,0 +1,57 @@ +extends SceneTree + +## The incident report contract: +## - a meltdown with breakage produces a report with the right total +## - a meltdown with NO breakage produces NOTHING (no empty receipt) +## +## Godot --headless --path game --script dev/probe_receipt.gd + +func _initialize() -> void: + var main: Node = (load("res://main.tscn") as PackedScene).instantiate() + get_root().add_child(main) + await process_frame + for i in 20: + await physics_frame + + var rage = main.get("_rage") + var receipt: Receipt = main.get("_receipt") + if rage == null or receipt == null: + print("FAIL: rage or receipt missing") + quit(1) + return + + # --- case 1: empty meltdown -> no report --- + rage._snap() + await physics_frame + rage._calm() + await physics_frame + if receipt.visible: + print("FAIL: empty meltdown produced a receipt") + quit(1) + return + print("empty meltdown -> no receipt: OK") + + # --- case 2: break three things during a meltdown --- + rage._snap() + await physics_frame + main._on_smashed("glass", Vector3.ZERO, 8) # $40 + main._on_smashed("glass", Vector3.ZERO, 8) # $40 + main._on_smashed("paper", Vector3.ZERO, 3) # $5 + rage._calm() + await physics_frame + if not receipt.visible: + print("FAIL: meltdown with breakage produced no receipt") + quit(1) + return + # assert on the built lines (the animated label reveals over real seconds, which a + # near-instant headless frame loop won't advance) + var txt: String = "\n".join(receipt._lines) + print("--- receipt ---") + print(txt) + print("---------------") + if txt.contains("TOTAL DAMAGES") and txt.contains("85") and txt.contains("Glazing") and txt.contains("x2"): + print("RECEIPT OK") + quit(0) + else: + print("FAIL: receipt total/lines wrong (expected $85, glass x2)") + quit(1) diff --git a/game/dev/probe_receipt.gd.uid b/game/dev/probe_receipt.gd.uid new file mode 100644 index 0000000..247d6e6 --- /dev/null +++ b/game/dev/probe_receipt.gd.uid @@ -0,0 +1 @@ +uid://c6jm65v60uwu2 diff --git a/game/dev/probe_roomba.gd.uid b/game/dev/probe_roomba.gd.uid new file mode 100644 index 0000000..0295201 --- /dev/null +++ b/game/dev/probe_roomba.gd.uid @@ -0,0 +1 @@ +uid://ruhqul0qbp3w diff --git a/game/scripts/Main.gd b/game/scripts/Main.gd index 6a2c092..16c9110 100644 --- a/game/scripts/Main.gd +++ b/game/scripts/Main.gd @@ -44,6 +44,7 @@ var _dust: Dust var _backrooms: Backrooms var _splat: Splat var _roomba: Roomba # the debris-eating antagonist — see Roomba.gd +var _receipt: Receipt # itemized damage report at the end of each meltdown func _ready() -> void: randomize() @@ -72,6 +73,12 @@ func _ready() -> void: _shift.setup(_rage, _tasks) _shift.begin() + # HR itemizes each meltdown the moment it ends — see Receipt.gd + _receipt = Receipt.new() + add_child(_receipt) + _rage.snapped.connect(_receipt.begin_window) + _rage.calmed.connect(_receipt.show_report) + # the provocations you can't play around: flickering tube, fish in the microwave _ambience = Ambience.new() add_child(_ambience) @@ -721,6 +728,9 @@ func _on_smashed(kind: String, at: Vector3, shards: int) -> void: _rage.on_smashed() if _shift != null: _shift.on_smashed(int(POINTS.get(kind, 15))) + # itemize it if it happened mid-meltdown — the receipt prints when rage calms + if _receipt != null and _rage != null and _rage.is_meltdown(): + _receipt.add(kind, int(POINTS.get(kind, 15))) if _modes != null: _modes.on_smashed(int(POINTS.get(kind, 15))) # LEVEL 0: the entity hunts sound, and breaking things is the objective. Playing well diff --git a/game/scripts/Receipt.gd b/game/scripts/Receipt.gd new file mode 100644 index 0000000..1316627 --- /dev/null +++ b/game/scripts/Receipt.gd @@ -0,0 +1,164 @@ +extends CanvasLayer +class_name Receipt + +## The incident report. When a meltdown ends, HR itemizes what you broke DURING that +## window and prints it, line by line, on green-bar paper: OBJECT · unit $ · ×N, then +## the total, then DEDUCTED FROM PAY. It's the comedy payoff the payslip earns — a +## reading of score data you already have, with a dot-matrix chirp per line. +## +## No textures, no fonts on disk, no audio assets. Any key or five seconds dismisses it. + +const UNIT := { + "paper": 5, "cardboard": 10, "plastic": 20, "vinyl": 25, + "wood": 30, "glass": 40, "steel": 60, "produce": 8, +} +const LABEL := { + "paper": "Loose paper", "cardboard": "Cardboard box", "plastic": "Moulded plastic", + "vinyl": "Vinyl record", "wood": "Timber furniture", "glass": "Glazing / bottle", + "steel": "Steel fixture", "produce": "Fresh produce", +} +const LINE_S := 0.11 ## seconds between printed lines + +var _tally: Dictionary = {} ## kind -> count, for the window in progress +var _panel: PanelContainer +var _label: Label +var _lines: PackedStringArray +var _shown := 0 +var _tick: Timer +var _dismiss: Timer +var _chirp: AudioStreamWAV +var _printer: AudioStreamPlayer +var _active := false + +func _ready() -> void: + layer = 80 + visible = false + _panel = PanelContainer.new() + _panel.anchor_left = 0.5 + _panel.anchor_top = 0.5 + _panel.anchor_right = 0.5 + _panel.anchor_bottom = 0.5 + _panel.grow_horizontal = Control.GROW_DIRECTION_BOTH + _panel.grow_vertical = Control.GROW_DIRECTION_BOTH + var sb := StyleBoxFlat.new() + sb.bg_color = Color(0.87, 0.90, 0.80) # green-bar computer paper + sb.border_color = Color(0.20, 0.22, 0.16) + sb.set_border_width_all(2) + sb.set_content_margin_all(18) + sb.shadow_color = Color(0, 0, 0, 0.5) + sb.shadow_size = 12 + _panel.add_theme_stylebox_override("panel", sb) + add_child(_panel) + + _label = Label.new() + var mono := SystemFont.new() + mono.font_names = PackedStringArray(["Menlo", "SF Mono", "Courier New", "monospace"]) + _label.add_theme_font_override("font", mono) + _label.add_theme_font_size_override("font_size", 17) + _label.add_theme_color_override("font_color", Color(0.12, 0.14, 0.10)) + _panel.add_child(_label) + + _tick = Timer.new() + _tick.wait_time = LINE_S + _tick.timeout.connect(_print_next) + add_child(_tick) + _dismiss = Timer.new() + _dismiss.one_shot = true + _dismiss.wait_time = 5.0 + _dismiss.timeout.connect(hide_report) + add_child(_dismiss) + + _chirp = _make_chirp() + _printer = AudioStreamPlayer.new() + _printer.stream = _chirp + _printer.volume_db = -8.0 + add_child(_printer) + +## Wire: rage.snapped -> begin_window, rage.calmed -> show_report; Main.add() each smash +## that happened while is_meltdown(). +func begin_window() -> void: + _tally.clear() + +func add(kind: String, _value: int) -> void: + _tally[kind] = int(_tally.get(kind, 0)) + 1 + +func show_report() -> void: + if _tally.is_empty(): + return # snapped and calmed without breaking anything — no receipt + _lines = PackedStringArray() + _lines.append("MONSTER ROBOT PARTY · INCIDENT REPORT") + _lines.append("employee outburst — itemized") + _lines.append("----------------------------------------") + var total := 0 + var kinds := _tally.keys() + kinds.sort() + for k in kinds: + var n := int(_tally[k]) + var unit := int(UNIT.get(k, 15)) + var sub := unit * n + total += sub + _lines.append("%-18s $%3d x%-2d $%5d" % [LABEL.get(k, k.capitalize()), unit, n, sub]) + _lines.append("----------------------------------------") + _lines.append("%-28s $%5d" % ["TOTAL DAMAGES", total]) + _lines.append("%-28s -$%4d" % ["DEDUCTED FROM PAY", total]) + _lines.append("") + _lines.append(" [ any key ]") + + _shown = 0 + _label.text = "" + visible = true + _active = true + _dismiss.stop() + _tick.start() + +func _print_next() -> void: + if _shown >= _lines.size(): + _tick.stop() + _dismiss.start() # fully printed — now the 5 s auto-dismiss begins + return + _label.text += ("" if _shown == 0 else "\n") + _lines[_shown] + _shown += 1 + if _lines[_shown - 1] != "": + _printer.pitch_scale = randf_range(0.96, 1.06) + _printer.play() + +func hide_report() -> void: + visible = false + _active = false + _tick.stop() + _dismiss.stop() + +func _unhandled_input(event: InputEvent) -> void: + if not _active: + return + if (event is InputEventKey and event.pressed) or (event is InputEventMouseButton and event.pressed): + # a key mid-print finishes it instantly; a key once printed dismisses it + if _shown < _lines.size(): + _label.text = "\n".join(_lines) + _shown = _lines.size() + _tick.stop() + _dismiss.start() + else: + hide_report() + get_viewport().set_input_as_handled() + +## A short square-wave blip — the sound of an impact printer landing one line. +func _make_chirp() -> AudioStreamWAV: + var rate := 22050 + var n := int(rate * 0.045) + var out := PackedFloat32Array() + out.resize(n) + for i in n: + var t := float(i) / float(rate) + var env := exp(-t * 55.0) + out[i] = (1.0 if sin(TAU * 1400.0 * t) > 0.0 else -1.0) * 0.3 * env + var w := AudioStreamWAV.new() + w.format = AudioStreamWAV.FORMAT_16_BITS + w.mix_rate = rate + w.stereo = false + var bytes := PackedByteArray() + bytes.resize(n * 2) + for i in n: + bytes.encode_s16(i * 2, int(clampf(out[i], -1.0, 1.0) * 32767.0)) + w.data = bytes + return w diff --git a/game/scripts/Receipt.gd.uid b/game/scripts/Receipt.gd.uid new file mode 100644 index 0000000..3e12fde --- /dev/null +++ b/game/scripts/Receipt.gd.uid @@ -0,0 +1 @@ +uid://cpavbgpt0kos