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 if String(task.get("kind", "")) == "spreadsheet": _work_grid.text = _spreadsheet_grid(task) elif 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 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