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"] 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() 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 func _label(parent: Control, size: int, col: Color, shadow := true) -> Label: var l := Label.new() 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) _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 # ---------------------------------------------------------------- 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: _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", "")) _a_mode.text = str(_g("mode_line", "")) _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 — THE RECORD STORE") 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