destroyulator/game/scripts/Receipt.gd
Monster Robot Party fe5a7d4422 LANE9: the incident report — HR itemizes your meltdown
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 <noreply@anthropic.com>
2026-08-04 00:05:33 +10:00

165 lines
5.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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