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>
165 lines
5.1 KiB
GDScript
165 lines
5.1 KiB
GDScript
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
|