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>
186 lines
6.4 KiB
GDScript
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
|