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>
651 lines
20 KiB
GDScript
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
|