destroyulator/game/scripts/Hud.gd
Monster Robot Party 81c9c4a349 LANE8: scales that swing, bags that don't open, and fruit you can throw
The three things the greengrocer was still missing, plus the physics bug that
finding them uncovered.

HANGING SCALES. Real PinJoint3D pendulums, not animations: a static dial and
rod, and a scale-pan rigid body pinned at its own origin so it can only rotate
about the pivot. They hang at chest height down the aisles — dial at eye level,
dish below — where a real shop scale hangs and where you keep walking into it.
The dish swings 0.40 m off a light knock and the pin holds the pivot to 1.1 mm.
The pan is steel so it never breaks; what you get is a heavy brass weight loose
in a room full of stacked fruit.

THE BAG YOU CANNOT OPEN. Tearing one off the roll is the easy half — that's the
setup. Then you have to open it, and the bag has two ends, only one of which
opens, and nothing tells you which. Rubbing alternates arrow keys, because
mashing one key is not rubbing. Either the meter climbs or it doesn't, and the
only way to learn which end you're holding is to have already lost several
seconds to the other one. SPACE turns it over. That is the whole solution and
the game never says so.

CARRY AND THROW. E picks up anything under 6 kg that isn't a record; LMB throws
it at 11 m/s, six times any brittle threshold in the game. Thrown fruit bursts
on landing through the same brittle_speed path a collapse uses — no separate
thrown-object code at all.

Then the part that took the longest. Round produce got sphere colliders (a box
on an apple is why nothing ever rolled) and every display in the shop instantly
fell over. Three separate things were wrong:

  - the stack radii were TYPED, not measured. The table said a cabbage was
    84 mm; the model is 213 mm, so that pyramid was built with every row driven
    a third of the way into the row below it. Main._glb_radius() reads the asset.
  - the lattice was box geometry. For spheres the rise per row is
    sqrt(4r^2 - step^2/2) — the height at which a fruit touches all four
    beneath it. Anything else spawns every row above the first in mid-air.
  - there was no tray. A pyramid of spheres on a bare flat table cannot stand;
    nothing holds the bottom row in, so the weight above wedges it outward and
    the display walks itself apart in a second. Main._stack_tray() frames each
    pile in four low timber walls sized to its base row, which is what every
    greengrocer on earth already does.

All 310 bodies asleep within 3 s.

While chasing that, the spawn guard fired on a cardboard box in the OFFICE. The
guard is a net, not a test: it only catches a pair Jolt happens to resolve
violently on the frames it's watching, and this one had been interpenetrating
in four levels for weeks. dev/probe_overlap.gd now finds them by measurement,
comparing every pair of dynamic colliders across all six sites. It found 15,
including a row of filing cabinets 0.70 m apart that are 0.80 m wide, and a
stapler inside a monitor. The box asset is 1.35 m across, so boxes are now
stacked into piles rather than dotted about, and the Backrooms scatter uses
rejection sampling with a 1.6 m minimum. All six sites read CLEAN.

Also: the rage veins were drawing every branch from its own start point
regardless of how far the trunk had grown, so at low rage you got disconnected
fragments floating mid-screen that read as biro scribble rather than blood. A
branch now can't appear before the trunk carrying it, and trunks are thick and
dark where capillaries are fine and pale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:22:47 +10:00

651 lines
20 KiB
GDScript

extends CanvasLayer
class_name Hud
## Four selectable HUD styles over one shared data feed. Cycle with H.
##
## ARCADE ...... big score, combo meter, weapon card. The default: this is a
## score-attack game about a rising combo, so the combo is the loudest
## thing on screen.
## MINIMAL ..... crosshair-adjacent only. For screenshots and for people who want the
## room, not the numbers. Combo appears only while it's alive.
## WORK ORDER .. the joke, played straight: a corporate destruction docket that fills
## in line items as you wreck the place. Same data, funnier framing.
## DEV ......... FPS / body count / weapon internals. What the prototype used to show.
##
## Main pushes a plain Dictionary in via `feed()` once a frame; every style reads the
## same keys, so adding a style never means touching the game code.
##
## Godot 4.7 GDScript 2.0.
enum Style { ARCADE, MINIMAL, WORKORDER, DEV }
const STYLE_NAMES := ["ARCADE", "MINIMAL", "WORK ORDER", "DEV"]
# The task panel, the interaction prompt and the payslip sit OUTSIDE the style system:
# they're the CUBICLE HELL mode's own furniture and have to be visible whatever HUD
# skin is selected, so they get their own always-on layer.
var _work_root: Control
var _work_panel: ColorRect
var _work_title: Label
var _work_instr: Label
var _work_grid: Label
var _work_foot: Label
var _work_bar_bg: ColorRect
var _work_bar: ColorRect
var _prompt: Label
var _slip_root: Control
var _slip: Label
const PINK := Color(1.0, 0.36, 0.62)
const CREAM := Color(0.94, 0.92, 0.86)
const INK := Color(0.11, 0.10, 0.12)
var style: int = Style.ARCADE
var _data: Dictionary = {}
var _roots: Array[Control] = []
# --- arcade widgets
var _a_score: Label
var _a_combo: Label
var _a_combo_bar: ColorRect
var _a_combo_bg: ColorRect
var _a_weapon: Label
var _a_weapon_sub: Label
var _a_mode: Label
var _a_toast: Label
var _toast_left := 0.0
# --- minimal
var _m_combo: Label
var _m_weapon: Label
# --- work order
var _w_lines: Label
var _w_head: Label
# --- dev
var _d_text: Label
func _ready() -> void:
layer = 10
_build_arcade()
_build_minimal()
_build_workorder()
_build_dev()
_apply()
_build_work()
_build_payslip()
func cycle() -> void:
style = (style + 1) % STYLE_NAMES.size()
_apply()
toast("HUD: %s" % STYLE_NAMES[style])
func _apply() -> void:
for i in _roots.size():
_roots[i].visible = (i == style)
## Main calls this every frame. Keys: score, combo, smashed, bodies, fps, weapon,
## weapon_blurb, slot, mode_line, time_left, tally (Dictionary kind->count).
func feed(d: Dictionary) -> void:
_data = d
## A transient centre-screen message (weapon pickup, HUD change, futile-hit hint).
func toast(msg: String) -> void:
if _a_toast != null:
_a_toast.text = msg
_toast_left = 1.7
# ---------------------------------------------------------------- helpers
## A monospace face for anything drawn as a text grid. Godot's fallback font is
## proportional, so column padding with %-9s lands wherever it likes — the spreadsheet
## and the work-order docket both need real fixed pitch to line up.
static func _mono() -> Font:
var f := SystemFont.new()
f.font_names = PackedStringArray(["Menlo", "SF Mono", "Courier New", "monospace"])
return f
func _label(parent: Control, size: int, col: Color, shadow := true, mono := false) -> Label:
var l := Label.new()
if mono:
l.add_theme_font_override("font", _mono())
l.add_theme_font_size_override("font_size", size)
l.add_theme_color_override("font_color", col)
if shadow:
l.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.75))
l.add_theme_constant_override("shadow_offset_x", 2)
l.add_theme_constant_override("shadow_offset_y", 2)
parent.add_child(l)
return l
func _panel(parent: Control, col: Color) -> ColorRect:
var r := ColorRect.new()
r.color = col
parent.add_child(r)
return r
func _root() -> Control:
var c := Control.new()
c.set_anchors_preset(Control.PRESET_FULL_RECT)
c.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(c)
_roots.append(c)
return c
# ---------------------------------------------------------------- ARCADE
func _build_arcade() -> void:
var r := _root()
_a_score = _label(r, 54, Color.WHITE)
_a_score.anchor_left = 0.5
_a_score.anchor_right = 0.5
_a_score.offset_left = -260
_a_score.offset_right = 260
_a_score.offset_top = 14
_a_score.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_a_combo = _label(r, 26, PINK)
_a_combo.anchor_left = 0.5
_a_combo.anchor_right = 0.5
_a_combo.offset_left = -260
_a_combo.offset_right = 260
_a_combo.offset_top = 74
_a_combo.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_a_combo_bg = _panel(r, Color(1, 1, 1, 0.13))
_a_combo_bg.anchor_left = 0.5
_a_combo_bg.anchor_right = 0.5
_a_combo_bg.offset_left = -130
_a_combo_bg.offset_right = 130
_a_combo_bg.offset_top = 108
_a_combo_bg.offset_bottom = 116
_a_combo_bar = _panel(r, PINK)
_a_combo_bar.anchor_left = 0.5
_a_combo_bar.anchor_right = 0.5
_a_combo_bar.offset_left = -130
_a_combo_bar.offset_right = -130
_a_combo_bar.offset_top = 108
_a_combo_bar.offset_bottom = 116
# weapon card, bottom right
var card := _panel(r, Color(0, 0, 0, 0.42))
card.anchor_left = 1.0
card.anchor_right = 1.0
card.anchor_top = 1.0
card.anchor_bottom = 1.0
card.offset_left = -330
card.offset_right = -18
card.offset_top = -84
card.offset_bottom = -18
_a_weapon = _label(r, 25, Color.WHITE)
_a_weapon.anchor_left = 1.0
_a_weapon.anchor_right = 1.0
_a_weapon.anchor_top = 1.0
_a_weapon.anchor_bottom = 1.0
_a_weapon.offset_left = -318
_a_weapon.offset_right = -26
_a_weapon.offset_top = -78
_a_weapon.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_a_weapon_sub = _label(r, 14, Color(1, 1, 1, 0.62))
_a_weapon_sub.anchor_left = 1.0
_a_weapon_sub.anchor_right = 1.0
_a_weapon_sub.anchor_top = 1.0
_a_weapon_sub.anchor_bottom = 1.0
_a_weapon_sub.offset_left = -318
_a_weapon_sub.offset_right = -26
_a_weapon_sub.offset_top = -46
_a_weapon_sub.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_a_weapon_sub.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_a_mode = _label(r, 16, Color(1, 1, 1, 0.78))
_a_mode.anchor_top = 1.0
_a_mode.anchor_bottom = 1.0
_a_mode.offset_left = 20
_a_mode.offset_top = -44
_a_toast = _label(r, 22, Color.WHITE)
_a_toast.anchor_left = 0.5
_a_toast.anchor_right = 0.5
_a_toast.anchor_top = 0.5
_a_toast.anchor_bottom = 0.5
_a_toast.offset_left = -300
_a_toast.offset_right = 300
_a_toast.offset_top = 84
_a_toast.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
# ---------------------------------------------------------------- MINIMAL
func _build_minimal() -> void:
var r := _root()
_m_combo = _label(r, 22, PINK)
_m_combo.anchor_left = 0.5
_m_combo.anchor_right = 0.5
_m_combo.anchor_top = 0.5
_m_combo.anchor_bottom = 0.5
_m_combo.offset_left = -160
_m_combo.offset_right = 160
_m_combo.offset_top = 46
_m_combo.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_m_weapon = _label(r, 15, Color(1, 1, 1, 0.5))
_m_weapon.anchor_left = 1.0
_m_weapon.anchor_right = 1.0
_m_weapon.anchor_top = 1.0
_m_weapon.anchor_bottom = 1.0
_m_weapon.offset_left = -260
_m_weapon.offset_right = -20
_m_weapon.offset_top = -36
_m_weapon.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
# ---------------------------------------------------------------- WORK ORDER
func _build_workorder() -> void:
var r := _root()
# tall enough for the header + 7 material rows + the totals block; the docket runs
# off the bottom of a shorter panel once you've broken one of everything
var paper := _panel(r, CREAM)
paper.offset_left = 18
paper.offset_top = 16
paper.offset_right = 372
paper.offset_bottom = 330
var strip := _panel(r, INK)
strip.offset_left = 18
strip.offset_top = 16
strip.offset_right = 372
strip.offset_bottom = 52
_w_head = _label(r, 15, CREAM, false)
_w_head.offset_left = 30
_w_head.offset_top = 24
_w_head.offset_right = 366
_w_lines = _label(r, 14, INK, false, true)
_w_lines.offset_left = 30
_w_lines.offset_top = 62
_w_lines.offset_right = 362
_w_lines.offset_bottom = 322
# ---------------------------------------------------------------- DEV
func _build_dev() -> void:
var r := _root()
_d_text = _label(r, 16, PINK)
_d_text.offset_left = 16
_d_text.offset_top = 12
# ---------------------------------------------------------------- work panel
## The thing you stare at while doing your job. Deliberately drab: a beige box with a
## monospace grid, so that when the vignette goes red around it the contrast lands.
func _build_work() -> void:
_work_root = Control.new()
_work_root.set_anchors_preset(Control.PRESET_FULL_RECT)
_work_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
_work_root.visible = false
add_child(_work_root)
_work_panel = _panel(_work_root, Color(0.90, 0.89, 0.84, 0.97))
_work_panel.anchor_left = 0.5
_work_panel.anchor_right = 0.5
_work_panel.anchor_top = 0.5
_work_panel.anchor_bottom = 0.5
_work_panel.offset_left = -300
_work_panel.offset_right = 300
_work_panel.offset_top = -180
_work_panel.offset_bottom = 150
var bar := _panel(_work_root, INK)
bar.anchor_left = 0.5
bar.anchor_right = 0.5
bar.anchor_top = 0.5
bar.anchor_bottom = 0.5
bar.offset_left = -300
bar.offset_right = 300
bar.offset_top = -180
bar.offset_bottom = -146
_work_title = _label(_work_root, 15, CREAM, false)
_work_title.anchor_left = 0.5
_work_title.anchor_right = 0.5
_work_title.anchor_top = 0.5
_work_title.offset_left = -288
_work_title.offset_right = 288
_work_title.offset_top = -173
_work_instr = _label(_work_root, 19, INK, false)
_work_instr.anchor_left = 0.5
_work_instr.anchor_right = 0.5
_work_instr.anchor_top = 0.5
_work_instr.offset_left = -288
_work_instr.offset_right = 288
_work_instr.offset_top = -134
_work_instr.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_work_grid = _label(_work_root, 16, INK, false, true)
_work_grid.anchor_left = 0.5
_work_grid.anchor_right = 0.5
_work_grid.anchor_top = 0.5
_work_grid.offset_left = -288
_work_grid.offset_right = 288
_work_grid.offset_top = -96
_work_bar_bg = _panel(_work_root, Color(0, 0, 0, 0.18))
_work_bar_bg.anchor_left = 0.5
_work_bar_bg.anchor_right = 0.5
_work_bar_bg.anchor_top = 0.5
_work_bar_bg.offset_left = -250
_work_bar_bg.offset_right = 250
_work_bar_bg.offset_top = -30
_work_bar_bg.offset_bottom = -8
_work_bar = _panel(_work_root, Color(0.16, 0.42, 0.22))
_work_bar.anchor_left = 0.5
_work_bar.anchor_right = 0.5
_work_bar.anchor_top = 0.5
_work_bar.offset_left = -250
_work_bar.offset_right = -250
_work_bar.offset_top = -30
_work_bar.offset_bottom = -8
_work_foot = _label(_work_root, 13, Color(0.35, 0.34, 0.32), false)
_work_foot.anchor_left = 0.5
_work_foot.anchor_right = 0.5
_work_foot.anchor_top = 0.5
_work_foot.offset_left = -288
_work_foot.offset_right = 288
_work_foot.offset_top = 122
_work_foot.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_prompt = _label(_work_root, 18, Color.WHITE)
_prompt.anchor_left = 0.5
_prompt.anchor_right = 0.5
_prompt.anchor_top = 0.5
_prompt.anchor_bottom = 0.5
_prompt.offset_left = -240
_prompt.offset_right = 240
_prompt.offset_top = 52
_prompt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
func _build_payslip() -> void:
_slip_root = Control.new()
_slip_root.set_anchors_preset(Control.PRESET_FULL_RECT)
_slip_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
_slip_root.visible = false
add_child(_slip_root)
var p := _panel(_slip_root, CREAM)
p.anchor_left = 0.5
p.anchor_right = 0.5
p.anchor_top = 0.5
p.anchor_bottom = 0.5
p.offset_left = -230
p.offset_right = 230
p.offset_top = -200
p.offset_bottom = 200
_slip = _label(_slip_root, 15, INK, false, true)
_slip.anchor_left = 0.5
_slip.anchor_right = 0.5
_slip.anchor_top = 0.5
_slip.offset_left = -206
_slip.offset_right = 206
_slip.offset_top = -178
## Main pushes the task/prompt/payslip state here, separately from `feed`, because it
## has to render regardless of which HUD skin is selected.
func feed_work(task: Dictionary, prompt_text: String, payslip: PackedStringArray) -> void:
if _work_root == null:
return
var busy := not task.is_empty()
var slip := payslip.size() > 0
_slip_root.visible = slip
if slip:
_slip.text = "\n".join(payslip)
_work_root.visible = (busy or prompt_text != "") and not slip
_work_panel.visible = busy
_work_title.visible = busy
_work_instr.visible = busy
_work_grid.visible = busy
_work_foot.visible = busy
_prompt.visible = (not busy) and prompt_text != ""
_prompt.text = prompt_text
var show_bar := busy and task.has("progress")
_work_bar_bg.visible = show_bar
_work_bar.visible = show_bar
if not busy:
return
_work_title.text = String(task.get("title", ""))
_work_instr.text = String(task.get("instruction", ""))
_work_foot.text = String(task.get("footer", ""))
if show_bar:
var f := clampf(float(task["progress"]), 0.0, 1.0)
_work_bar.offset_right = -250.0 + 500.0 * f
match String(task.get("kind", "")):
"spreadsheet":
_work_grid.text = _spreadsheet_grid(task)
"staples":
_work_grid.text = _staples_art(task)
"guillotine":
_work_grid.text = _guillotine_art(task)
"bags":
_work_grid.text = _bags_art(task)
_:
if bool(task.get("jam", false)):
var trays: Array = task.get("trays", [])
var line := " ".join(PackedStringArray(
trays.map(func(t): return "[%s]" % t)))
_work_grid.text = "\n TRAYS: " + line
else:
_work_grid.text = ""
## The bundle, drawn as text. The arrow only appears once the staple is lifted, so you
## can't pre-empt it — you lift, read, and pull, under a clock.
func _staples_art(task: Dictionary) -> String:
var armed := bool(task.get("armed", false))
var d := String(task.get("dir", "?"))
var urg := float(task.get("urgency", 0.0))
var bar := ""
var n := int(round(urg * 22.0))
for i in 22:
bar += "#" if i < n else "."
var out := PackedStringArray()
out.append("")
out.append(" +--------------------------+")
out.append(" | |")
if armed:
out.append(" | << %s >> |" % d)
else:
out.append(" | [ lift ] |")
out.append(" | |")
out.append(" +--------------------------+")
out.append("")
out.append(" tearing: " + bar)
return "\n".join(out)
## The bag. Both ends are drawn IDENTICALLY and the art never hints which one opens —
## because in a real shop it doesn't either. All it tells you is which end you're
## currently working, so that when you finally turn it over you know you turned it over.
func _bags_art(task: Dictionary) -> String:
if int(task.get("stage", 0)) == 0:
return "\n .-------------------------.\n" \
+ " ( ((((((((( o )))))))))) )\n" \
+ " '-------------------------'\n" \
+ " | |\n" \
+ " ___|_|___\n" \
+ " | torn |\n" \
+ " '---------'\n"
var e := int(task.get("end", 0))
var g := float(task.get("grip", 0.0))
const L := 5 # left edge column of the bag
const W := 30 # bag width in characters
var pointer := " ".repeat(L + (1 if e == 0 else W - 2)) + "V"
var wall := " ".repeat(L) + "#".repeat(W)
var mid := " ".repeat(L) + "#" + " ".repeat(W - 2) + "#"
var bar := ""
var n := int(round(g * 26.0))
for i in 26:
bar += "#" if i < n else "."
return "\n".join([
pointer, wall, mid, mid, wall, "",
" ".repeat(L) + "holding the %s end" % ("LEFT" if e == 0 else "RIGHT"),
" ".repeat(L) + "grip [" + bar + "]",
])
## A rule, a mark, and the sheet. The blade does NOT land where the sheet sits — the
## guide has a permanent offset — and nothing on screen tells you that.
func _guillotine_art(task: Dictionary) -> String:
var target := float(task.get("target", 0.5))
var at := float(task.get("at", 0.5))
const W := 46
var rule := ""
for i in W:
rule += "|" if i % 5 == 0 else "-"
var marks := ""
var ti := int(round(target * (W - 1)))
for i in W:
marks += "v" if i == ti else " "
var sheet := ""
var ai := int(round(at * (W - 1)))
for i in W:
sheet += "#" if i == ai else ("=" if absi(i - ai) < 6 else " ")
var out := PackedStringArray()
out.append("")
out.append(" " + marks)
out.append(" " + rule)
out.append(" " + sheet)
out.append("")
out.append(" the guide has always been a little out")
return "\n".join(out)
## The spreadsheet, drawn as text. The selected cell is bracketed — which is the ONLY
## indication that it has quietly moved, and that is entirely the point.
func _spreadsheet_grid(task: Dictionary) -> String:
var cols: Array = task.get("cols", [])
var rows: int = int(task.get("rows", 0))
var sel: Vector2i = task.get("sel", Vector2i.ZERO)
var typed: String = String(task.get("typed", ""))
var out := PackedStringArray()
var head := " "
for c in cols:
head += "%-9s" % String(c)
out.append(head)
for r in range(rows):
var line := "%-5s" % str(r + 1)
for c in range(cols.size()):
if Vector2i(c, r) == sel:
line += "%-9s" % ("[" + (typed if typed != "" else "_") + "]")
else:
line += "%-9s" % "."
out.append(line)
return "\n".join(out)
# ---------------------------------------------------------------- per-frame
func _process(dt: float) -> void:
if _data.is_empty():
return
_toast_left = maxf(0.0, _toast_left - dt)
match style:
Style.ARCADE:
_tick_arcade()
Style.MINIMAL:
_tick_minimal()
Style.WORKORDER:
_tick_workorder()
Style.DEV:
_tick_dev()
func _g(k: String, dflt = 0):
return _data.get(k, dflt)
func _tick_arcade() -> void:
# in CUBICLE HELL the headline number is your payslip, not an arcade score —
# net pay is the thing you're actually playing against
var state := String(_g("rage_state", ""))
if state != "":
var net := int(_g("net"))
_a_score.text = ("$%s" % _comma(net)) if net >= 0 else ("-$%s" % _comma(-net))
_a_score.add_theme_color_override("font_color",
Color(1, 0.42, 0.38) if net < 0 else Color.WHITE)
else:
_a_score.text = "%s" % _comma(int(_g("score")))
var combo := int(_g("combo"))
if combo > 1:
_a_combo.text = "COMBO x%d" % combo
_a_combo.modulate.a = 1.0
else:
_a_combo.text = ""
var decay := float(_g("combo_decay", 0.0)) # 1 = fresh, 0 = about to drop
_a_combo_bg.visible = combo > 1
_a_combo_bar.visible = combo > 1
_a_combo_bar.offset_right = -130.0 + 260.0 * clampf(decay, 0.0, 1.0)
_a_weapon.text = "%d · %s" % [int(_g("slot")) + 1, str(_g("weapon", ""))]
_a_weapon_sub.text = str(_g("weapon_blurb", ""))
var mode := str(_g("mode_line", ""))
var status := str(_g("rage_status", ""))
if status != "":
mode += "\n" + status
_a_mode.text = mode
_a_toast.modulate.a = clampf(_toast_left / 0.5, 0.0, 1.0)
func _tick_minimal() -> void:
var combo := int(_g("combo"))
_m_combo.text = ("x%d" % combo) if combo > 1 else ""
_m_weapon.text = str(_g("weapon", ""))
func _tick_workorder() -> void:
_w_head.text = "MONSTER ROBOT PARTY · WORK ORDER #%04d" % int(_g("order_no", 4471))
var tally: Dictionary = _g("tally", {})
var lines := PackedStringArray()
lines.append("SITE: LEVEL 01 — BRANCH OFFICE, 2ND FLOOR")
lines.append("TOOL: %s" % str(_g("weapon", "")))
lines.append("")
lines.append("ITEM QTY")
lines.append("---------------------------- --")
var order := ["wood", "cardboard", "vinyl", "glass", "steel", "plastic", "paper"]
var any := false
for k in order:
var n := int(tally.get(k, 0))
if n <= 0:
continue
any = true
lines.append("%-27s %3d" % [_item_name(k), n])
if not any:
lines.append("(nothing logged yet)")
lines.append("")
lines.append("TOTAL UNITS DESTROYED %6d" % int(_g("smashed")))
lines.append("ASSESSED VALUE %6s" % _comma(int(_g("score"))))
var combo := int(_g("combo"))
if combo > 1:
lines.append("EFFICIENCY BONUS x%d" % combo)
_w_lines.text = "\n".join(lines)
func _item_name(kind: String) -> String:
match kind:
"wood": return "Shelving, timber"
"cardboard": return "Cartons, corrugated"
"vinyl": return "Stock, 12in vinyl"
"glass": return "Glazing / CRT"
"steel": return "Fixtures, steel"
"plastic": return "Fittings, moulded"
"paper": return "Paperwork"
return kind
func _tick_dev() -> void:
_d_text.text = "FPS %d bodies %d smashed %d score %d combo x%d\n%s\n%s\n%s" % [
int(_g("fps")), int(_g("bodies")), int(_g("smashed")),
int(_g("score")), int(_g("combo")),
"weapon: %s (slot %d) %s" % [str(_g("weapon", "-")), int(_g("slot")) + 1,
str(_g("weapon_stats", ""))],
str(_g("mode_line", "")),
str(_g("keys", ""))]
func _comma(n: int) -> String:
var s := str(absi(n))
var out := ""
var c := 0
for i in range(s.length() - 1, -1, -1):
out = s[i] + out
c += 1
if c % 3 == 0 and i > 0:
out = "," + out
return ("-" if n < 0 else "") + out