// Palladium: Bleach - CLIENT Animation Script v22
// v22: GETSUGA TENSHO ANIME SLASH — getsuga_tensho / black_getsuga get their
// own cinematic register (priority 148): raise the blade overhead, hold the
// charge, then a SLOW head-to-ground slash with body pitch + twist, timed so
// the crescent-wave particle blast (getsuga_fx last_tick in the power JSON)
// detonates exactly as the slash lands. Both removed from the beam list.
// v21: SPRINT/FP INVISIBILITY — REAL ROOT CAUSE FOUND. It was never the run
// pose: the DRAW BLADE battle stance (v16+) held RIGHT_ARM rotated FOREVER
// while the toggle stayed on, and SPIRITUAL PRESSURE held RIGHT_ARM at -90
// FOREVER while its toggle stayed on. Neither ever had a first-person guard,
// so the first-person hand was pushed off screen the whole time the toggle
// was active — which is exactly when the user sprints. Fixes:
//   (1) EVERY register now starts with a fail-closed THIRD-PERSON-ONLY guard
//       (tpOnly). First person gets ZERO custom rotations from this script.
//   (2) No animation holds an arm rotation forever anymore. DRAW plays its
//       sweep then releases to the vanilla pose (arms end at 0). SPIRITUAL
//       PRESSURE raises the arm for ~2s then lowers it (keyframed, ends 0).
//       SPEED BLITZ is now body-lean only. BLOCK keeps its raised arm but
//       only while the key is physically held (transient by nature).
// v20: run pose reduced to body lean + head tilt + sway (arm-free) — KEPT.
// v20: DRAW_KF reaches the LEFT hip (saya side) via Z-axis — KEPT.
// COPY THIS TO: .minecraft/kubejs/client_scripts/bleach_animations_client.js
// DELETE any old version first!
//
// Historical rules that still apply:
// - Arm X rotations capped -80..+60 (block/spiritual pressure may use -90/-15).
// - NEGATIVE X = raise arm forward/up. POSITIVE X = behind the back.
// - Each ability lives in exactly ONE animation list (dedup audit).

console.log('[Palladium: Bleach] Client animation script v24 loading...')

function lerp(a, b, t) { return a + (b - a) * t }
function easeInOut(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2)/2 }

// FAIL-CLOSED third-person check: rotations are ONLY applied when the builder
// positively confirms we are NOT rendering the first-person hand. If neither
// API answers, we skip the pose (no pose > broken pose). This runs at the top
// of EVERY register so first person can never inherit a custom rotation.
function tpOnly(builder) {
    try { return !builder.isFirstPerson() } catch(e) {
        try { return ('' + builder.getFirstPersonContext()) == 'NONE' } catch(e2) { return false }
    }
}

function getProgress(player, powerId, abilityKey, duration) {
    try {
        var entry = null
        try { entry = palladium.abilities.getEntry(player, powerId, abilityKey) } catch(e) {}
        if (entry != null) {
            try {
                var ticks = entry.getEnabledTicks()
                if (ticks > 0) return Math.min(ticks / duration, 1.0)
            } catch(e) {}
        }
        if (palladium.abilities.isEnabled(player, powerId, abilityKey)) return 0.5
        return -1
    } catch(e) { return -1 }
}

function isActive(player, powerId, abilityKey) {
    try { return palladium.abilities.isEnabled(player, powerId, abilityKey) }
    catch(e) { return false }
}

// For TOGGLE abilities: progress sweeps 0..1 exactly once per enable, then
// clamps at 1.0. v21 NOTE: callers must stop applying at 1.0 (or use keyframes
// that end at all-zero rotations) — holding a final keyframe forever is what
// used to blank the first-person hand.
function getToggleProgress(player, powerId, abilityKey, duration) {
    try {
        if (!palladium.abilities.isEnabled(player, powerId, abilityKey)) return -1
        var entry = null
        try { entry = palladium.abilities.getEntry(player, powerId, abilityKey) } catch(e) {}
        if (entry != null) {
            try {
                var ticks = entry.getEnabledTicks()
                return Math.min(ticks / duration, 1.0)
            } catch(e) {}
        }
        return 1.0
    } catch(e) { return -1 }
}

function applyKeyframes(builder, keyframes, progress) {
    if (progress < 0) return false
    var p = Math.max(0, Math.min(1, progress))
    var k1 = keyframes[0]
    var k2 = keyframes[keyframes.length - 1]
    for (var i = 0; i < keyframes.length - 1; i++) {
        if (p >= keyframes[i].t && p <= keyframes[i + 1].t) {
            k1 = keyframes[i]
            k2 = keyframes[i + 1]
            break
        }
    }
    var span = k2.t - k1.t
    var segT = span > 0 ? (p - k1.t) / span : 0
    var eased = easeInOut(segT)
    var partNames = {}
    for (var key in k1.parts) partNames[key] = true
    for (var key in k2.parts) partNames[key] = true
    for (var partName in partNames) {
        var p1 = k1.parts[partName] || { x: 0, y: 0, z: 0 }
        var p2 = k2.parts[partName] || { x: 0, y: 0, z: 0 }
        try {
            var part = builder.get(partName)
            if (part == null) continue
            if (p2.x !== undefined) part.setXRotDegrees(lerp(p1.x || 0, p2.x, eased))
            if (p2.y !== undefined) part.setYRotDegrees(lerp(p1.y || 0, p2.y, eased))
            if (p2.z !== undefined) part.setZRotDegrees(lerp(p1.z || 0, p2.z, eased))
        } catch(e) {}
    }
    return true
}

// ─── Keyframes ──────────────────────────────────────────────────────────────

// SWING — wind up (-80) → slash forward (-30) → recover to 0
var SWING_KF = [
    { t: 0.0,  parts: { RIGHT_ARM: { x: 0 },   BODY: { y: 0 },  LEFT_ARM: { x: 0 } } },
    { t: 0.2,  parts: { RIGHT_ARM: { x: -80 }, BODY: { y: -5 }, LEFT_ARM: { x: -20 } } },
    { t: 0.5,  parts: { RIGHT_ARM: { x: -80 }, BODY: { y: -5 }, LEFT_ARM: { x: -20 } } },
    { t: 0.7,  parts: { RIGHT_ARM: { x: -30 },  BODY: { y: 3 },  LEFT_ARM: { x: -10 } } },
    { t: 1.0,  parts: { RIGHT_ARM: { x: 0 },   BODY: { y: 0 },  LEFT_ARM: { x: 0 } } },
]

// SHUNDO — flash pose (-60) → slash (-25) → recover to 0
var SHUNDO_KF = [
    { t: 0.0,  parts: { RIGHT_ARM: { x: -60 }, BODY: { y: -8 }, LEFT_ARM: { x: -25 } } },
    { t: 0.3,  parts: { RIGHT_ARM: { x: -60 }, BODY: { y: -8 }, LEFT_ARM: { x: -25 } } },
    { t: 0.5,  parts: { RIGHT_ARM: { x: -25 }, BODY: { y: 3 },  LEFT_ARM: { x: -10 } } },
    { t: 0.8,  parts: { RIGHT_ARM: { x: -5 },  BODY: { y: 0 },  LEFT_ARM: { x: 0 } } },
    { t: 1.0,  parts: { RIGHT_ARM: { x: 0 },   BODY: { y: 0 },  LEFT_ARM: { x: 0 } } },
]

// GROUND SMASH — raise up (-80) → swing down to ground (+60) → recover to 0
var GROUND_KF = [
    { t: 0.0,  parts: { RIGHT_ARM: { x: 0 },   BODY: { x: 0, y: 0 } } },
    { t: 0.35, parts: { RIGHT_ARM: { x: -80 }, BODY: { x: -3, y: -12 } } },
    { t: 0.65, parts: { RIGHT_ARM: { x: 60 },  BODY: { x: 8, y: 10 } } },
    { t: 1.0,  parts: { RIGHT_ARM: { x: 0 },   BODY: { x: 0, y: 0 } } },
]

// DASH — forward lean, arms back slightly, ends at 0
var DASH_KF = [
    { t: 0.0,  parts: { BODY: { x: 0 },  RIGHT_ARM: { x: 0 },  LEFT_ARM: { x: 0 } } },
    { t: 0.2,  parts: { BODY: { x: 15 }, RIGHT_ARM: { x: -30 }, LEFT_ARM: { x: -30 } } },
    { t: 0.7,  parts: { BODY: { x: 15 }, RIGHT_ARM: { x: -30 }, LEFT_ARM: { x: -30 } } },
    { t: 1.0,  parts: { BODY: { x: 0 },  RIGHT_ARM: { x: 0 },  LEFT_ARM: { x: 0 } } },
]

// BEAM CHARGE — both arms forward (-45), ends at 0
var BEAM_KF = [
    { t: 0.0,  parts: { RIGHT_ARM: { x: 0, z: 0 },    LEFT_ARM: { x: 0, z: 0 } } },
    { t: 0.2,  parts: { RIGHT_ARM: { x: -25, z: -6 }, LEFT_ARM: { x: -25, z: 6 } } },
    { t: 0.4,  parts: { RIGHT_ARM: { x: -45, z: -12 }, LEFT_ARM: { x: -45, z: 12 } } },
    { t: 0.8,  parts: { RIGHT_ARM: { x: -45, z: -12 }, LEFT_ARM: { x: -45, z: 12 } } },
    { t: 1.0,  parts: { RIGHT_ARM: { x: 0, z: 0 },    LEFT_ARM: { x: 0, z: 0 } } },
]

// BANKAI — both arms raised (-60), ends at 0
var BANKAI_KF = [
    { t: 0.0,  parts: { RIGHT_ARM: { x: 0, z: 0 },     LEFT_ARM: { x: 0, z: 0 },    BODY: { x: 0 }, HEAD: { x: 0 } } },
    { t: 0.25, parts: { RIGHT_ARM: { x: -60, z: 18 },  LEFT_ARM: { x: -60, z: -18 }, BODY: { x: -5 }, HEAD: { x: -18 } } },
    { t: 0.75, parts: { RIGHT_ARM: { x: -60, z: 18 },  LEFT_ARM: { x: -60, z: -18 }, BODY: { x: -5 }, HEAD: { x: -18 } } },
    { t: 1.0,  parts: { RIGHT_ARM: { x: 0, z: 0 },     LEFT_ARM: { x: 0, z: 0 },    BODY: { x: 0 }, HEAD: { x: 0 } } },
]

// DRAW BLADE (iaijutsu) — coil toward the LEFT hip (saya side), snap out wide,
// then RELEASE fully to zero. v21: the held "battle stance" is gone — holding
// an arm rotation for as long as the toggle stays on is what made the
// first-person hand invisible. The sweep plays once (~1.2s), then vanilla pose.
var DRAW_KF = [
    { t: 0.0,  parts: { RIGHT_ARM: { x: 0, z: 0 },     LEFT_ARM: { x: 0, z: 0 },    BODY: { y: 0 } } },
    { t: 0.3,  parts: { RIGHT_ARM: { x: -45, z: -35 }, LEFT_ARM: { x: -20, z: 18 }, BODY: { y: 20 } } },
    { t: 0.55, parts: { RIGHT_ARM: { x: -60, z: 25 },  LEFT_ARM: { x: -12, z: 8 },  BODY: { y: -12 } } },
    { t: 0.8,  parts: { RIGHT_ARM: { x: -35, z: 10 },  LEFT_ARM: { x: -10, z: 3 },  BODY: { y: -4 } } },
    { t: 1.0,  parts: { RIGHT_ARM: { x: 0, z: 0 },     LEFT_ARM: { x: 0, z: 0 },   BODY: { y: 0 } } },
]

// GETSUGA TENSHO — anime slash: raise overhead (0-30%), hold the charge
// (30-50%), SLOW downward slash (50-85%), recover (85-100%). Ends at zero.
// X range: -80..+50, inside the -80..+60 cap.
var GETSUGA_KF = [
    { t: 0.0,  parts: { RIGHT_ARM: { x: 0, z: 0 },    LEFT_ARM: { x: 0, z: 0 },   BODY: { x: 0, y: 0 },   HEAD: { x: 0 } } },
    { t: 0.3,  parts: { RIGHT_ARM: { x: -80, z: -10 }, LEFT_ARM: { x: -30, z: 6 }, BODY: { x: -8, y: 6 },  HEAD: { x: -10 } } },
    { t: 0.5,  parts: { RIGHT_ARM: { x: -80, z: -10 }, LEFT_ARM: { x: -30, z: 6 }, BODY: { x: -8, y: 6 },  HEAD: { x: -10 } } },
    { t: 0.85, parts: { RIGHT_ARM: { x: 50, z: 8 },    LEFT_ARM: { x: 20, z: 0 },  BODY: { x: 14, y: -14 }, HEAD: { x: 8 } } },
    { t: 1.0,  parts: { RIGHT_ARM: { x: 0, z: 0 },    LEFT_ARM: { x: 0, z: 0 },   BODY: { x: 0, y: 0 },   HEAD: { x: 0 } } },
]

// SPIRITUAL PRESSURE — v21: keyframed raise (-90, the user-approved pose) that
// HOLDS ~2 seconds then LOWERS back to zero. The toggle can stay on for hours;
// the arm no longer stays up (and can no longer hide the first-person hand).
var REIATSU_KF = [
    { t: 0.0,  parts: { RIGHT_ARM: { x: 0 },   LEFT_ARM: { x: 0 } } },
    { t: 0.12, parts: { RIGHT_ARM: { x: -90 }, LEFT_ARM: { x: -15 } } },
    { t: 0.72, parts: { RIGHT_ARM: { x: -90 }, LEFT_ARM: { x: -15 } } },
    { t: 1.0,  parts: { RIGHT_ARM: { x: 0 },   LEFT_ARM: { x: 0 } } },
]

// ─── Registration ───────────────────────────────────────────────────────────

PalladiumEvents.registerAnimations((event) => {
    console.log('[Palladium: Bleach] registerAnimations v22 — getsuga anime slash!')

    var ALL_POWERS = [
        'palladiumbleach:ichigo', 'palladiumbleach:ichigo_bankai', 'palladiumbleach:byakuya',
        'palladiumbleach:shunsui', 'palladiumbleach:aizen', 'palladiumbleach:soul_reaper',
        'palladiumbleach:kenpachi', 'palladiumbleach:kenpachi_bankai', 'palladiumbleach:rukia',
        'palladiumbleach:rukia_bankai', 'palladiumbleach:toshiro', 'palladiumbleach:toshiro_bankai',
        'palladiumbleach:grimmjow', 'palladiumbleach:arrancar', 'palladiumbleach:vizard',
        'palladiumbleach:kido', 'palladiumbleach:quincy',
        'palladiumbleach:ichibei', 'palladiumbleach:yhwach',
        'palladiumbleach:yamamoto', 'palladiumbleach:urahara', 'palladiumbleach:aizen_hogyoku',
        'palladiumbleach:quincy_vollstandig', 'palladiumbleach:gin'
    ]

    // BLOCK — RIGHT=-90, LEFT=-15 while the key is HELD (transient by nature).
    // Also applies to REIATSU BURST and SAVAGE AOE (short activation abilities).
    event.register('bleach_block', 200, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < ALL_POWERS.length; i++) {
                if (isActive(player, ALL_POWERS[i], 'block') || isActive(player, ALL_POWERS[i], 'reiatsu_burst') || isActive(player, ALL_POWERS[i], 'savage_aoe')) {
                    builder.get('RIGHT_ARM').setXRotDegrees(-90)
                    builder.get('LEFT_ARM').setXRotDegrees(-15)
                    return
                }
            }
        } catch(e) {}
    })

    // BANKAI / TRANSFORMATION (priority 150)
    event.register('bleach_bankai', 150, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var bankaiAbilities = [
                ['palladiumbleach:ichigo', 'hollow_mask', 30],
                ['palladiumbleach:ichigo', 'bankai_release', 30],
                ['palladiumbleach:ichigo_bankai', 'vizard_form', 30],
                ['palladiumbleach:ichigo_bankai', 'seal_bankai', 30],
                ['palladiumbleach:kenpachi', 'bloodlust', 30],
                ['palladiumbleach:kenpachi', 'bankai_release', 30],
                ['palladiumbleach:kenpachi_bankai', 'true_berserker', 30],
                ['palladiumbleach:grimmjow', 'resurreccion', 30],
                ['palladiumbleach:vizard', 'hollow_form', 30],
                ['palladiumbleach:soul_reaper', 'shikai_release', 30],
                ['palladiumbleach:aizen', 'kyoka_release', 30],
                ['palladiumbleach:quincy', 'letzt_stil', 30],
                ['palladiumbleach:arrancar', 'resurreccion', 30],
                ['palladiumbleach:rukia', 'bankai_release', 30],
                ['palladiumbleach:rukia_bankai', 'seal_bankai', 30],
                ['palladiumbleach:toshiro', 'bankai_release', 30],
                ['palladiumbleach:toshiro_bankai', 'seal_bankai', 30],
                ['palladiumbleach:ichibei', 'name_severing', 30],
                ['palladiumbleach:yhwach', 'the_almighty', 30],
                ['palladiumbleach:yamamoto', 'bankai_release', 30],
                ['palladiumbleach:urahara', 'shikai_release', 30],
                ['palladiumbleach:aizen_hogyoku', 'hogyoku_fusion', 40],
                ['palladiumbleach:quincy_vollstandig', 'vollstandig', 60],
            ]
            for (var i = 0; i < bankaiAbilities.length; i++) {
                var arr = bankaiAbilities[i]
                var prog = getProgress(player, arr[0], arr[1], arr[2])
                if (prog >= 0) {
                    applyKeyframes(builder, BANKAI_KF, prog)
                    return
                }
            }
        } catch(e) {}
    })

    // DRAW BLADE — iaijutsu draw from the left hip, then release (priority 145).
    // 'draw_blade' appears ONLY in this list. Applies only while prog < 1.0 —
    // once the sweep finishes the animation fully lets go of the model.
    var SWORD_POWERS = [
        'palladiumbleach:asauchi', 'palladiumbleach:soul_reaper', 'palladiumbleach:ichigo',
        'palladiumbleach:ichigo_bankai', 'palladiumbleach:byakuya', 'palladiumbleach:shunsui',
        'palladiumbleach:aizen', 'palladiumbleach:aizen_hogyoku', 'palladiumbleach:gin',
        'palladiumbleach:kenpachi', 'palladiumbleach:kenpachi_bankai', 'palladiumbleach:rukia',
        'palladiumbleach:rukia_bankai', 'palladiumbleach:toshiro', 'palladiumbleach:toshiro_bankai',
        'palladiumbleach:yamamoto', 'palladiumbleach:urahara', 'palladiumbleach:yhwach',
        'palladiumbleach:ichibei', 'palladiumbleach:vizard'
    ]
    event.register('bleach_draw', 145, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < SWORD_POWERS.length; i++) {
                var prog = getToggleProgress(player, SWORD_POWERS[i], 'draw_blade', 24)
                if (prog >= 0 && prog < 1.0) {
                    applyKeyframes(builder, DRAW_KF, prog)
                    return
                }
            }
        } catch(e) {}
    })

    // GETSUGA TENSHO — cinematic overhead slash (priority 148). These two
    // abilities live ONLY here (removed from the beam list).
    event.register('bleach_getsuga', 148, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var getsugaAbilities = [
                ['palladiumbleach:ichigo', 'getsuga_tensho', 14],
                ['palladiumbleach:ichigo_bankai', 'black_getsuga', 30],
            ]
            for (var i = 0; i < getsugaAbilities.length; i++) {
                var arr = getsugaAbilities[i]
                var prog = getProgress(player, arr[0], arr[1], arr[2])
                if (prog >= 0) {
                    applyKeyframes(builder, GETSUGA_KF, prog)
                    return
                }
            }
        } catch(e) {}
    })

    // BEAM CHARGE — both arms forward (priority 140)
    event.register('bleach_beam_charge', 140, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var beamAbilities = [
                ['palladiumbleach:byakuya', 'scatter_petals', 30],
                ['palladiumbleach:byakuya', 'shukei_hakuteiken', 30],
                ['palladiumbleach:byakuya', 'kageyoshi', 30],
                ['palladiumbleach:shunsui', 'bushogoma', 30],
                ['palladiumbleach:shunsui', 'karamatsu_shinju', 30],
                ['palladiumbleach:aizen', 'illusion_beam', 30],
                ['palladiumbleach:aizen', 'complete_hypnosis_special', 30],
                ['palladiumbleach:kenpachi', 'savage_slash', 30],
                ['palladiumbleach:kenpachi_bankai', 'devastation_slash', 30],
                ['palladiumbleach:grimmjow', 'garra', 30],
                ['palladiumbleach:grimmjow', 'desgarron', 30],
                ['palladiumbleach:ichigo', 'final_getsuga', 20],
                ['palladiumbleach:ichigo_bankai', 'mugetsu', 30],
                ['palladiumbleach:aizen', 'false_slash', 20],
                ['palladiumbleach:aizen', 'complete_illusion', 20],
                ['palladiumbleach:kenpachi', 'rampage', 20],
                ['palladiumbleach:byakuya', 'petal_storm', 20],
                ['palladiumbleach:soul_reaper', 'shakkaho', 30],
                ['palladiumbleach:soul_reaper', 'kurohitsugi', 30],
                ['palladiumbleach:arrancar', 'gran_rey_cero', 30],
                ['palladiumbleach:arrancar', 'cero', 30],
                ['palladiumbleach:vizard', 'cero', 30],
                ['palladiumbleach:vizard', 'cero_oscuras', 30],
                ['palladiumbleach:rukia', 'hakuren', 30],
                ['palladiumbleach:rukia', 'ryusenka', 30],
                ['palladiumbleach:rukia_bankai', 'hakka_no_togame', 30],
                ['palladiumbleach:toshiro', 'ryusenka', 30],
                ['palladiumbleach:toshiro_bankai', 'daiguren_hyorinmaru', 30],
                ['palladiumbleach:kido', 'hado_31', 20],
                ['palladiumbleach:kido', 'hado_88', 20],
                ['palladiumbleach:kido', 'bakudo_61', 20],
                ['palladiumbleach:kido', 'bakudo_81', 20],
                ['palladiumbleach:quincy', 'spirit_bow', 10],
                ['palladiumbleach:quincy_vollstandig', 'spirit_bow', 10],
                ['palladiumbleach:ichibei', 'ink_slash', 30],
                ['palladiumbleach:ichibei', 'ichimonji', 40],
                ['palladiumbleach:yhwach', 'reishi_slash', 30],
                ['palladiumbleach:yhwach', 'sankt_altar', 40],
                ['palladiumbleach:yamamoto', 'flame_slash', 30],
                ['palladiumbleach:yamamoto', 'zanka_no_tachi', 40],
                ['palladiumbleach:urahara', 'blood_mist', 30],
                ['palladiumbleach:aizen_hogyoku', 'reality_warp', 40],
                ['palladiumbleach:gin', 'shoot_to_kill', 25],
                ['palladiumbleach:gin', 'butou_renjin', 35],
            ]
            for (var i = 0; i < beamAbilities.length; i++) {
                var arr = beamAbilities[i]
                var prog = getProgress(player, arr[0], arr[1], arr[2])
                if (prog >= 0) {
                    applyKeyframes(builder, BEAM_KF, prog)
                    return
                }
            }
        } catch(e) {}
    })

    // SHUNDO SLASH (priority 130)
    event.register('bleach_shundo_slash', 130, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var shundoAbilities = [
                ['palladiumbleach:ichigo', 'shundo_slash', 30],
                ['palladiumbleach:ichigo_bankai', 'bankai_shundo', 30],
            ]
            for (var i = 0; i < shundoAbilities.length; i++) {
                var arr = shundoAbilities[i]
                var prog = getProgress(player, arr[0], arr[1], arr[2])
                if (prog >= 0) {
                    applyKeyframes(builder, SHUNDO_KF, prog)
                    return
                }
            }
        } catch(e) {}
    })

    // GROUND SLASH (priority 120)
    event.register('bleach_ground_slash', 120, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < ALL_POWERS.length; i++) {
                var prog = getProgress(player, ALL_POWERS[i], 'ground_slash', 20)
                if (prog >= 0) {
                    applyKeyframes(builder, GROUND_KF, prog)
                    return
                }
            }
        } catch(e) {}
    })

    // SWING — overhead slash (priority 100)
    event.register('bleach_swing', 100, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var swingAbilities = [
                ['palladiumbleach:kido', 'hado_63', 20],
            ]
            for (var i = 0; i < swingAbilities.length; i++) {
                var arr = swingAbilities[i]
                var prog = getProgress(player, arr[0], arr[1], arr[2])
                if (prog >= 0) {
                    applyKeyframes(builder, SWING_KF, prog)
                    return
                }
            }
        } catch(e) {}
    })

    // SHUNPO / DASH (priority 90)
    event.register('bleach_shunpo', 90, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var dashAbilities = [
                ['palladiumbleach:ichigo', 'shunpo', 20],
                ['palladiumbleach:ichigo', 'reverse_flash', 20],
                ['palladiumbleach:ichigo_bankai', 'enhanced_shunpo', 20],
                ['palladiumbleach:kenpachi', 'berserk_dash', 20],
                ['palladiumbleach:kenpachi_bankai', 'berserk_charge', 20],
                ['palladiumbleach:grimmjow', 'sonido', 20],
                ['palladiumbleach:arrancar', 'sonido', 20],
                ['palladiumbleach:byakuya', 'petal_teleport', 20],
                ['palladiumbleach:shunsui', 'shadow_ambush', 20],
                ['palladiumbleach:quincy', 'hirenkyaku', 20],
                ['palladiumbleach:yhwach', 'hirenkyaku', 20],
                ['palladiumbleach:urahara', 'shunpo', 20],
                ['palladiumbleach:quincy_vollstandig', 'hirenkyaku', 20],
                ['palladiumbleach:gin', 'korose_shinso', 20],
            ]
            for (var i = 0; i < dashAbilities.length; i++) {
                var arr = dashAbilities[i]
                var prog = getProgress(player, arr[0], arr[1], arr[2])
                if (prog >= 0) {
                    applyKeyframes(builder, DASH_KF, prog)
                    return
                }
            }
        } catch(e) {}
    })

    // SPEED BLITZ — toggle, continuous lean. v21: BODY + HEAD ONLY (the arm
    // hold was the same class of bug as the draw stance).
    event.register('bleach_speed_blitz', 88, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < ALL_POWERS.length; i++) {
                if (isActive(player, ALL_POWERS[i], 'speed_blitz')) {
                    var wobble = Math.sin(player.tickCount * 0.3) * 2
                    builder.get('BODY').setXRotDegrees(18 + wobble)
                    builder.get('HEAD').setXRotDegrees(-15)
                    return
                }
            }
        } catch(e) {}
    })

    // SPIRITUAL PRESSURE — raise arm, hold ~2s, lower (priority 85).
    // v21: keyframed transient instead of a forever-hold.
    event.register('bleach_reiatsu', 85, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < ALL_POWERS.length; i++) {
                var prog = getToggleProgress(player, ALL_POWERS[i], 'spiritual_pressure', 50)
                if (prog >= 0 && prog < 1.0) {
                    applyKeyframes(builder, REIATSU_KF, prog)
                    return
                }
            }
        } catch(e) {}
    })

    // NOTE: Jinzen "sit" is done server-side by mounting an invisible seat
    // (see the jinzen ability in asauchi/soul_reaper powers) — a client leg/body
    // pose here caused the first-person view to blank, so it was removed.

    // ANIME RUN removed: the sprint body/head lean was blanking the first-person
    // view (same failure class as the old jinzen client pose). Sprinting now uses
    // the vanilla animation - no invisibility.

    console.log('[Palladium: Bleach] All 12 animations registered (v24 — jinzen sit moved server-side)!')
})
