destroyulator/game/scripts/Weapon.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

186 lines
6.4 KiB
GDScript

extends Resource
class_name Weapon
## One weapon: its stats, its swing, and how it sits in the hands.
##
## Replaces MeleeAttack. The important new part is `vs` — the WEAPON vs MATERIAL
## matrix the founding doc asked for. A weapon no longer just "breaks things"; it does
## `power * vs[material]` damage against that material's toughness (Smashable.PROFILES
## .hp). That is what makes the loadout a decision instead of a skin:
##
## box cutter — shreds cardboard and paper, pings uselessly off a filing cabinet
## cricket bat — the all-rounder; snaps vinyl and glass, struggles with steel
## crowbar — the only mid-speed answer to steel
## sledgehammer— ends anything, but you swing it once a second
##
## Swing timing matters as much as damage: `contact` is how far into the animation the
## hit actually lands, so the sledge connects late and heavy while the cutter is instant.
##
## Godot 4.7 GDScript 2.0.
@export var id: String = "fists"
@export var display_name: String = "Fists"
@export_multiline var blurb: String = ""
## Viewmodel mesh; "" means bare hands.
@export var mesh_path: String = ""
# ---------------------------------------------------------------- hit shape
@export var reach: float = 1.7 ## metres in front of the camera the hit sphere sits
@export var radius: float = 0.34 ## fat sphere — melee is forgiving, not pixel-precise
@export var impulse: float = 6.0 ## physics shove applied to whatever is hit
@export var knockback: float = 2.5 ## off-centre impulse for plain (non-Smashable) bodies
# ---------------------------------------------------------------- damage
@export var power: float = 1.0 ## base damage, before the material multiplier
@export var vs: Dictionary = {} ## material -> multiplier (missing = 1.0)
# ---------------------------------------------------------------- feel
@export var cooldown: float = 0.30 ## seconds between swings
@export var swing: String = "horizontal" ## horizontal | overhead | thrust | jab
@export var swing_time: float = 0.42 ## full animation length
@export var contact: float = 0.34 ## fraction of swing_time at which the hit lands
@export var two_handed: bool = false
@export var heft: float = 1.0 ## scales bob/sway amplitude and landing dip
@export var shake: float = 0.20 ## trauma added to the camera on a connecting hit
## Damage this weapon does to one material.
func damage_against(kind: String) -> float:
return power * float(vs.get(kind, 1.0))
## True when this weapon is essentially useless against `kind` — drives the comedy
## "clank, nothing happened" feedback instead of a silent non-event.
func is_futile_against(kind: String, hp: float) -> bool:
return damage_against(kind) < hp * 0.10
# ---------------------------------------------------------------- the loadout
## Built in code (not .tres) so the whole matrix reads as one table you can tune in
## a single place. Order here is the order of the number keys.
static func loadout() -> Array[Weapon]:
var out: Array[Weapon] = []
var fists := Weapon.new()
fists.id = "fists"
fists.display_name = "Bare Hands"
fists.blurb = "Free. Fast. Regrettable."
fists.mesh_path = ""
fists.reach = 1.65
fists.radius = 0.34
fists.power = 1.0
fists.impulse = 5.0
fists.knockback = 2.2
fists.cooldown = 0.26
fists.swing = "jab"
fists.swing_time = 0.30
fists.contact = 0.30
fists.heft = 0.5
fists.shake = 0.14
fists.vs = {"cardboard": 1.6, "paper": 2.0, "glass": 1.2, "vinyl": 1.0,
"wood": 0.45, "steel": 0.25, "plastic": 0.8}
out.append(fists)
var cutter := Weapon.new()
cutter.id = "cutter"
cutter.display_name = "Box Cutter"
cutter.blurb = "Shreds paper and card. Does nothing to furniture. Nothing."
cutter.mesh_path = "res://assets/viewmodel/cutter.glb"
cutter.reach = 1.45
cutter.radius = 0.28
cutter.power = 0.55
cutter.impulse = 2.0
cutter.knockback = 1.0
cutter.cooldown = 0.15
cutter.swing = "jab"
cutter.swing_time = 0.22
cutter.contact = 0.36
cutter.heft = 0.35
cutter.shake = 0.06
cutter.vs = {"cardboard": 9.0, "paper": 14.0, "vinyl": 2.6, "plastic": 2.0,
"glass": 0.30, "wood": 0.14, "steel": 0.04}
out.append(cutter)
var bat := Weapon.new()
bat.id = "bat"
bat.display_name = "Cricket Bat"
bat.blurb = "The all-rounder. Vinyl and glass do not enjoy it."
bat.mesh_path = "res://assets/viewmodel/bat.glb"
bat.reach = 2.10
bat.radius = 0.40
bat.power = 2.4
bat.impulse = 8.0
bat.knockback = 4.0
bat.cooldown = 0.40
bat.swing = "horizontal"
bat.swing_time = 0.44
bat.contact = 0.36
bat.two_handed = false
bat.heft = 1.0
bat.shake = 0.22
bat.vs = {"vinyl": 2.1, "glass": 1.7, "cardboard": 1.4, "wood": 1.2,
"plastic": 1.3, "steel": 0.55, "paper": 0.6}
out.append(bat)
var crowbar := Weapon.new()
crowbar.id = "crowbar"
crowbar.display_name = "Crowbar"
crowbar.blurb = "Bites metal. The filing cabinet has met its match."
crowbar.mesh_path = "res://assets/viewmodel/crowbar.glb"
crowbar.reach = 1.95
crowbar.radius = 0.34
crowbar.power = 3.0
crowbar.impulse = 9.0
crowbar.knockback = 4.5
crowbar.cooldown = 0.46
crowbar.swing = "overhead"
crowbar.swing_time = 0.50
crowbar.contact = 0.40
crowbar.heft = 1.15
crowbar.shake = 0.26
crowbar.vs = {"steel": 1.9, "wood": 1.4, "glass": 1.3, "cardboard": 1.2,
"vinyl": 1.2, "plastic": 1.5, "paper": 0.5}
out.append(crowbar)
var sledge := Weapon.new()
sledge.id = "sledge"
sledge.display_name = "Sledgehammer"
sledge.blurb = "One swing a second. Worth the wait."
sledge.mesh_path = "res://assets/viewmodel/sledge.glb"
sledge.reach = 2.25
sledge.radius = 0.46
sledge.power = 6.5
sledge.impulse = 15.0
sledge.knockback = 8.0
sledge.cooldown = 0.85
sledge.swing = "overhead"
sledge.swing_time = 0.78
sledge.contact = 0.46
sledge.two_handed = true
sledge.heft = 1.9
sledge.shake = 0.46
sledge.vs = {"wood": 1.7, "glass": 1.5, "steel": 1.35, "plastic": 1.5,
"vinyl": 1.2, "cardboard": 0.75, "paper": 0.4}
out.append(sledge)
var ext := Weapon.new()
ext.id = "extinguisher"
ext.display_name = "Fire Extinguisher"
ext.blurb = "Heavy, awkward, and the best thing in the room to throw."
ext.mesh_path = "res://assets/viewmodel/extinguisher.glb"
ext.reach = 1.85
ext.radius = 0.44
ext.power = 4.5
ext.impulse = 13.0
ext.knockback = 7.0
ext.cooldown = 0.70
ext.swing = "horizontal"
ext.swing_time = 0.66
ext.contact = 0.42
ext.two_handed = true
ext.heft = 1.6
ext.shake = 0.38
ext.vs = {"glass": 1.9, "steel": 1.1, "wood": 1.0, "plastic": 1.4,
"vinyl": 1.1, "cardboard": 0.9, "paper": 0.4}
out.append(ext)
return out