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>
This commit is contained in:
parent
32b556ff02
commit
fe5a7d4422
57
game/dev/probe_receipt.gd
Normal file
57
game/dev/probe_receipt.gd
Normal file
@ -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)
|
||||||
1
game/dev/probe_receipt.gd.uid
Normal file
1
game/dev/probe_receipt.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://c6jm65v60uwu2
|
||||||
1
game/dev/probe_roomba.gd.uid
Normal file
1
game/dev/probe_roomba.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://ruhqul0qbp3w
|
||||||
@ -44,6 +44,7 @@ var _dust: Dust
|
|||||||
var _backrooms: Backrooms
|
var _backrooms: Backrooms
|
||||||
var _splat: Splat
|
var _splat: Splat
|
||||||
var _roomba: Roomba # the debris-eating antagonist — see Roomba.gd
|
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:
|
func _ready() -> void:
|
||||||
randomize()
|
randomize()
|
||||||
@ -72,6 +73,12 @@ func _ready() -> void:
|
|||||||
_shift.setup(_rage, _tasks)
|
_shift.setup(_rage, _tasks)
|
||||||
_shift.begin()
|
_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
|
# the provocations you can't play around: flickering tube, fish in the microwave
|
||||||
_ambience = Ambience.new()
|
_ambience = Ambience.new()
|
||||||
add_child(_ambience)
|
add_child(_ambience)
|
||||||
@ -721,6 +728,9 @@ func _on_smashed(kind: String, at: Vector3, shards: int) -> void:
|
|||||||
_rage.on_smashed()
|
_rage.on_smashed()
|
||||||
if _shift != null:
|
if _shift != null:
|
||||||
_shift.on_smashed(int(POINTS.get(kind, 15)))
|
_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:
|
if _modes != null:
|
||||||
_modes.on_smashed(int(POINTS.get(kind, 15)))
|
_modes.on_smashed(int(POINTS.get(kind, 15)))
|
||||||
# LEVEL 0: the entity hunts sound, and breaking things is the objective. Playing well
|
# LEVEL 0: the entity hunts sound, and breaking things is the objective. Playing well
|
||||||
|
|||||||
164
game/scripts/Receipt.gd
Normal file
164
game/scripts/Receipt.gd
Normal file
@ -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
|
||||||
1
game/scripts/Receipt.gd.uid
Normal file
1
game/scripts/Receipt.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://cpavbgpt0kos
|
||||||
Loading…
Reference in New Issue
Block a user