THE GAUNTLET (Gauntlet.gd + Cabinet.gd)
Five identical cabinets, each with its alphabetical range on a Label3D. The brief names
a CLIENT, never a cabinet, so working out that HENDERSON lives in F-J is the puzzle —
no waypoint, nothing glows. Destruction is granular on purpose: pull a drawer, it
slides out on a spring and goes live, and the folders inside become individual
smashables. Doing that by hand inside the clock isn't really possible.
The perfect tool: every site hides a SHREDDER that eats a whole open drawer at once,
and the game never says so. Noticing it and working out what it's for IS the level.
Getting it wrong isn't fatal, just slow — the right shape for a secret, since it
rewards the observant without hard-failing everyone else.
Each site nominates its own records area in its spec. Building the bank 3.4 m in front
of wherever the player happened to be standing put it through walls.
MODES (Modes.gd, M cycles)
CUBICLE HELL the main loop
THE GAUNTLET above
TOTAL DESTRUCTION no job, no pay, clear the site, scored on time
AGAINST THE CLOCK fixed timer, maximise damage VALUE — the exact inverse of Cubicle
Hell, where expensive things become the goal rather than the
mistake. Same level, opposite instinct.
Entering a mode arms only what that mode needs; Cubicle Hell and the Gauntlet are
mutually exclusive, because you cannot be doing your job and shredding the Henderson
file at the same time.
TWO MORE PROVOCATIONS (Tasks.gd)
STAPLES SPACE lifts a staple, then it must be pulled the way it's bent — and it's
bent differently each time. Dither and the sheet tears, bundle reprints.
GUILLOTINE align to a mark and cut. The guide has a PERMANENT offset and nothing on
screen admits it. Once you work out the error it's easy, which is the
joke: you have to learn to distrust the equipment.
Desks now alternate spreadsheet/staples so "a desk" isn't always the same minigame.
AMBIENCE (Ambience.gd) — the attrition you can't play around. The tube above your desk
flickers, and actually flickers the real lights. Fish in the microwave. A phone ringing
four desks away. Reply-all, forty-one recipients, "thanks!". Small periodic ticks with
no interaction at all; it's what makes the room hostile rather than merely beige.
Fixes: the payslip was rendering over the Gauntlet (it belongs to Cubicle Hell, and only
once the shift has actually ended); gauntlet drawer reach was 2.0 m when the true
distance from standing-at-a-cabinet to a bottom drawer is over 2 m.
dev/probe_gauntlet.gd drives a whole run headlessly — mode switch, labels, open every
drawer, shred, win — because that catches the failure that matters: a run you can't
finish because the files never become destroyable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
620 lines
19 KiB
GDScript
620 lines
19 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)
|
|
_:
|
|
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)
|
|
|
|
## 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
|