extends Object class_name WeaponHoldProfiles ## How each weapon in the set is HELD — one archetype per class of weapon, ## rather than one rifle hold for all twelve. ## ## ── The problem this exists to fix ─────────────────────────────────────────── ## ## `ShooterPoseModifier._apply_rifle_hold` did exactly what its name says, to ## everything. A knife, a rocket launcher and an AK were all solved as a rifle: ## stock in the shoulder pocket, support hand out along the barrel, muzzle on the ## aim line. In third person every character therefore stood in the same pose ## regardless of what they were carrying, and the ONLY thing distinguishing a ## sniper from a shotgun was the ~30 cm of gun mesh in their hands — which at the ## distance an enemy is usually seen is nothing. ## ## That matters beyond looking wrong. In a shooter the reason a character's pose ## is readable at range is that it is the fastest available answer to "what is ## about to happen to me". A shouldered tube means take cover; a blade held low ## means they have to close the distance; a rifle at low ready means they have not ## seen you yet. A single hold throws all of that away. ## ## It is also the specific thing HoYoverse's team say they chase in Zenless Zone ## Zero: characters read by SILHOUETTE first, and their designers deliberately ## refuse to settle on one construction method because a single method limits how ## distinguishable the results can be. The same argument applies one level down, ## to how a character holds a thing. ## ## ── What a profile controls ───────────────────────────────────────────────── ## ## A style is not a bundle of slider values. Three of the differences below ## cannot be expressed as a number on the existing rifle solve at all, and those ## are the ones that make the silhouette: ## ## support where the off hand goes, and how it is ORIENTED there — ## wrapped round a handguard, cupped under a pistol grip, hooked ## under a tube, or released entirely so the animation owns it ## mount whether the weapon's rear sits IN the shoulder pocket, ON TOP ## of the shoulder, or nowhere near it ## head whether the head comes down to the stock (a cheek weld) or ## leans away to clear a tube ## ## Everything else — the pocket offsets, the muzzle pitch at low ready, the elbow ## poles, the finger curls — is an ordinary knob, and the profile just supplies a ## better DEFAULT for that weapon than one global constant could. ## ## ── Layering ──────────────────────────────────────────────────────────────── ## ## This is a defaults layer, underneath everything an artist has tuned: ## ## code constants the rifle solve's own fallbacks ## THIS FILE per weapon: what kind of thing it is ## weapon_holds.json defaults -> skins.._all -> skins.. ## ## So a character with tuning saved from the rig lab is completely unaffected — ## aria's hand-tuned AK-47 hold still wins on every knob it sets — and a weapon ## nobody has tuned stops being held like an AK. # ── The styles ─────────────────────────────────────────────────────────────── const RIFLE := "rifle" const SMG := "smg" const SNIPER := "sniper" const SHOTGUN := "shotgun" const LAUNCHER := "launcher" const PISTOL := "pistol" const BLADE := "blade" # ── Where the support hand goes, and how it is turned ──────────────────────── ## Wrapped round a handguard, fingers closing ACROSS the barrel. The rifle case. const SUPPORT_BARREL := "barrel" ## Cupped under and around the firing fist. Two hands together, no shoulder ## contact — a pistol, or any weapon light enough to be held out in front. const SUPPORT_CUPPED := "cupped" ## Hooked UNDER a tube from below, palm up, well forward of the shoulder. The ## launcher case, where there is no handguard to wrap and the weight is carried ## rather than aimed. const SUPPORT_TUBE := "tube" ## No support hand at all. The animation keeps the arm, which is what a character ## carrying a blade should look like — the off hand swings with the run cycle. const SUPPORT_FREE := "free" ## Style -> the structural rules for it. ## ## `pocket_hip` / `pocket_ads` are where the weapon's REAR sits, relative to the ## right shoulder joint, in skeleton space (x across, y up, z forward). They are ## the single most important number here: it is what decides whether a weapon ## reads as shouldered, carried, or held out. ## ## `cheek` is how far the head comes down and across to meet the stock, 0..1, ## applied only as the character shoulders the weapon. Negative leans the head ## AWAY, which is what a tube over the shoulder requires. ## ## `pitch_hip` is the muzzle's droop at low ready, in radians. A launcher's tube ## rides nose-UP because that is how you carry something you do not want pointed ## at your own feet; a blade points forward and in. const STYLES := { RIFLE: { "support": SUPPORT_BARREL, "pocket_hip": Vector3(0.03, -0.07, 0.06), "pocket_ads": Vector3(0.05, 0.01, 0.07), "pitch_hip": 0.16, "cheek": 0.18, "pole_r_hip": Vector3(-0.55, -0.85, -0.20), "pole_l_hip": Vector3(0.45, -0.90, -0.10), "curl_wrap": 1.0, "curl_trigger": 1.0, }, # Compact: the whole weapon is carried closer in, the support hand cannot go # far because there is not much gun in front of the grip, and the firing elbow # tucks rather than flares. This is what stops an MP7 from being posed as a # short rifle with the support arm reaching for a handguard that ended. SMG: { "support": SUPPORT_BARREL, "pocket_hip": Vector3(0.04, -0.10, 0.03), "pocket_ads": Vector3(0.05, 0.00, 0.05), "pitch_hip": 0.24, "cheek": 0.10, "pole_r_hip": Vector3(-0.40, -0.95, -0.15), "pole_l_hip": Vector3(0.35, -0.95, -0.05), "gun_fore": 0.17, "curl_wrap": 1.05, "curl_trigger": 1.0, }, # The cheek weld IS the sniper silhouette. The head comes down onto the stock, # the pocket sits high and tight so the optic lands at eye height, and the # support hand goes far out because a long barrel gives it somewhere to go. SNIPER: { "support": SUPPORT_BARREL, # Rides HIGH. A scoped rifle is held so the optic meets the eye, not so # the butt meets the pocket, and raising the whole weapon is what makes # that read — pushing the support hand further out does NOT, because the # reach solver slides it back down the handguard until the arm can get # there, so a longer `gun_fore` on these stylised arms lands the off hand # in exactly the same place as a rifle's. Measured: 0.388 vs 0.387 m. "pocket_hip": Vector3(0.015, 0.005, 0.045), "pocket_ads": Vector3(0.040, 0.075, 0.055), "pitch_hip": 0.12, "cheek": 0.85, "pole_r_hip": Vector3(-0.72, -0.55, -0.22), "pole_l_hip": Vector3(0.22, -1.05, -0.02), "gun_fore": 0.34, "curl_wrap": 1.0, "curl_trigger": 0.85, }, # Held lower and squarer than a rifle, with the support hand back on the # forend rather than out at the muzzle, and a hard wrap — a shotgun is gripped, # not balanced. SHOTGUN: { "support": SUPPORT_BARREL, # Carried low and FORWARD, the opposite of the sniper's high tuck. A # shotgun is pointed rather than aimed and the elbows go wide, because # the recoil comes back through them. # # Low and INBOARD does not work, however tempting the symmetry: +x is # toward the character's centreline, so dropping the pocket and pushing # it across at the same time swings the barrel through the chest. It is # visible immediately in debug/hold_capture.gd and in no assertion — # the hands were still exactly where they had been asked to go. "pocket_hip": Vector3(0.030, -0.130, 0.080), "pocket_ads": Vector3(0.045, -0.040, 0.075), "pitch_hip": 0.30, "cheek": 0.22, "pole_r_hip": Vector3(-0.62, -0.80, -0.22), "pole_l_hip": Vector3(0.58, -0.72, -0.16), "gun_fore": 0.22, "curl_wrap": 1.2, "curl_trigger": 1.0, }, # The tube goes ON the shoulder, not into it, and the head leans AWAY to clear # it. The support hand hooks under from below, well forward, because there is # nothing to wrap and the job of that arm is to carry weight. LAUNCHER: { "support": SUPPORT_TUBE, "pocket_hip": Vector3(0.055, 0.06, 0.01), "pocket_ads": Vector3(0.065, 0.105, 0.03), "pitch_hip": -0.16, "cheek": -0.30, "pole_r_hip": Vector3(-0.85, -0.45, -0.10), "pole_l_hip": Vector3(0.25, -0.85, 0.10), "gun_stock": 0.30, "gun_fore": 0.30, "curl_wrap": 1.0, "curl_trigger": 0.9, }, # No shoulder contact at all. The pocket is pushed forward and outboard of the # joint, which puts both arms out in front of the chest, and the support hand # cups the firing fist instead of reaching for a barrel. PISTOL: { "support": SUPPORT_CUPPED, "pocket_hip": Vector3(0.02, -0.16, 0.16), "pocket_ads": Vector3(0.00, -0.04, 0.30), "pitch_hip": 0.30, "cheek": 0.05, "pole_r_hip": Vector3(-0.45, -0.95, -0.05), "pole_l_hip": Vector3(0.45, -0.95, -0.05), "gun_stock": 0.03, "gun_fore": 0.05, "weapon_scale": 1.0, "curl_wrap": 1.15, "curl_trigger": 0.9, }, # One hand. The off arm is RELEASED back to the animation, so it swings with # the run cycle instead of gripping a handguard that does not exist — which is # most of what makes a knife read as a knife at any distance. BLADE: { "support": SUPPORT_FREE, "pocket_hip": Vector3(0.09, -0.20, 0.10), "pocket_ads": Vector3(0.05, -0.06, 0.22), "pitch_hip": 0.55, "cheek": 0.0, "pole_r_hip": Vector3(-0.35, -1.00, -0.10), "pole_l_hip": Vector3(0.45, -0.90, -0.10), "gun_stock": 0.02, "gun_fore": 0.0, "weapon_scale": 1.0, # A blade is held in a full fist — there is no trigger to keep a finger # straight along, and an extended index on a knife handle looks like a # mistake rather than like discipline. "curl_wrap": 1.25, "curl_trigger": 1.25, }, } ## Weapon id (the script's basename) -> style. ## ## Keyed on the basename rather than on the class, so this table does not have to ## load twelve weapon scripts to be read, and so a weapon that has not been ## written yet can be listed here the moment its file exists. const WEAPON_STYLES := { "ak47": RIFLE, "m4": RIFLE, "plasma_gun": RIFLE, "mp7": SMG, "nail_gun": SMG, "dmr": SNIPER, "awp": SNIPER, "double_barrel_shotgun": SHOTGUN, "rocket_launcher": LAUNCHER, "rocket_swarm": LAUNCHER, "mortar": LAUNCHER, "knife": BLADE, } ## What an unlisted weapon is held as. A rifle is the safe assumption: it is the ## only style that reaches for a handguard, and a weapon with no handguard held ## as a rifle looks odd, where a rifle held as anything else looks broken. const FALLBACK := RIFLE ## The style name for a weapon id or script path. static func style_for(weapon: String) -> String: var id := _id_of(weapon) return WEAPON_STYLES.get(id, FALLBACK) ## The default knob table for a weapon — the style's values, ready to be merged ## under whatever the JSON tuning says. ## ## Returns a COPY, because the caller merges the artist's values into it and a ## shared dictionary would accumulate one character's tuning into every other's. static func knobs_for(weapon: String) -> Dictionary: var style: Dictionary = STYLES.get(style_for(weapon), STYLES[FALLBACK]) var out := style.duplicate(true) # `support` and `cheek` are structural, not knobs — they are read straight # off the style by the pose layer and must not end up in the tuning table, # where the rig lab would offer sliders for them. out.erase("support") out.erase("cheek") return out ## How the off hand is used for this weapon. See the SUPPORT_* constants. static func support_for(weapon: String) -> String: var style: Dictionary = STYLES.get(style_for(weapon), STYLES[FALLBACK]) return style.get("support", SUPPORT_BARREL) ## How far the head comes to the stock, 0..1, negative to lean away. static func cheek_for(weapon: String) -> float: var style: Dictionary = STYLES.get(style_for(weapon), STYLES[FALLBACK]) return float(style.get("cheek", 0.0)) ## A weapon id from either an id or a `res://weapons/.gd` path. static func _id_of(weapon: String) -> String: if weapon.ends_with(".gd"): return weapon.get_file().get_basename() return weapon