destroyulator/game/scripts/Hud.gd
Monster Robot Party 61757d24bc LANE6: rigged FPS viewmodel, weapon loadout + material matrix, 4 HUD styles
Hands/POV
- Cut a real first-person arms rig out of the GODVERSE modular character kit
  (tools/gen_fps_arms.py): ch01 hands + per-side sleeves on the full 65-bone
  mixamorig skeleton, so all 20 finger bones per hand are poseable at runtime.
  Textures shrunk to 1k; 4.2 MB.
- ViewModel.gd instances that rig ONCE PER HAND and places each instance so its
  own hand bone lands on the grip — no IK. Grip orientation is measured off the
  rig at load (pinky->index knuckle = bore axis, elbow->hand = forearm dir), so
  it survives re-tuning grip_rest_rot instead of needing new euler angles.
- Motion layers: look-sway with spring-back, walk bob scaled by speed and weapon
  heft, idle breathe, landing dip, weapon lower/raise on swap.
- Sleeve material overridden (donor asset is a fantasy leather bracer); hand
  material forced non-metallic (its spec/gloss maps rendered skin as bronze).

Weapons
- Weapon.gd replaces MeleeAttack: 6 weapons, 4 swing archetypes, and the
  weapon-vs-material matrix from the founding chat.
- Smashable moves from binary hits_to_break to hp/toughness, giving three
  outcomes: break, dent, or futile (dead clank, no score, HUD nudge). The box
  cutter genuinely shreds cardboard and genuinely cannot hurt a filing cabinet.
- The hit lands at Weapon.contact THROUGH the swing, not on the click — that
  delay is most of why the sledge feels different from the cutter.
- Slots on 1-6 / wheel / Q; game modes moved to M, HUD cycle to H.

HUD
- Hud.gd: Arcade, Minimal, Work Order (a corporate destruction docket that fills
  in line items) and Dev, over one shared data feed. Scoring + combo multipliers.

Dev harness (not shipped)
- macOS screen-recording perms aren't available to the CLI, so the game records
  itself: dev/demo.tscn + DemoDriver.gd drive a scripted tour for --write-movie,
  and dev/probe_*.gd print rig/scale/placement numbers.

Fix: tools/gen_viewmodel.py box() scaled by size/2 on top of primitive_cube_add's
already-unit side length, halving every box — which detached the bat's blade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:34:00 +10:00

348 lines
10 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"]
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