// Grand Line: Devil Fruits - CLIENT animation script v1
// COPY THIS TO: .minecraft/kubejs/client_scripts/grandline_client.js
//
// Player-model animations for fruit abilities. Architecture rules inherited
// from the proven Palladium-Bleach client script:
//  - EVERY register starts with a fail-closed third-person guard (tpOnly):
//    first person gets ZERO custom rotations from this script.
//  - No animation holds a rotation forever; every keyframe track ends at 0.
//  - Arm X rotations kept within -80..+60 (negative X = raise arm forward).

console.log('[Grand Line] Client animation script v1 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
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 toggles: sweeps 0..1 once per enable then clamps; callers must end at 0 rotations
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 { return Math.min(entry.getEnabledTicks() / 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 (all tracks end at zero) ------------------------------------

// straight punch: coil back, snap forward, recover
var PUNCH_KF = [
    { t: 0.0, parts: { RIGHT_ARM: { x: 0 },   BODY: { y: 0 } } },
    { t: 0.25, parts: { RIGHT_ARM: { x: 35, z: 6 }, BODY: { y: -18 }, LEFT_ARM: { x: -12 } } },
    { t: 0.55, parts: { RIGHT_ARM: { x: -78, z: 0 }, BODY: { y: 16 }, LEFT_ARM: { x: 10 } } },
    { t: 0.8, parts: { RIGHT_ARM: { x: -40 }, BODY: { y: 6 } } },
    { t: 1.0, parts: { RIGHT_ARM: { x: 0 },  BODY: { y: 0 }, LEFT_ARM: { x: 0 } } }
]
// two-hand overhead slam
var SLAM_KF = [
    { t: 0.0, parts: { RIGHT_ARM: { x: 0 }, LEFT_ARM: { x: 0 }, BODY: { x: 0 } } },
    { t: 0.35, parts: { RIGHT_ARM: { x: -80 }, LEFT_ARM: { x: -80 }, BODY: { x: -8 }, HEAD: { x: -10 } } },
    { t: 0.6, parts: { RIGHT_ARM: { x: -15 }, LEFT_ARM: { x: -15 }, BODY: { x: 14 }, HEAD: { x: 8 } } },
    { t: 1.0, parts: { RIGHT_ARM: { x: 0 }, LEFT_ARM: { x: 0 }, BODY: { x: 0 }, HEAD: { x: 0 } } }
]
// dash lean: arms trail, body pitches
var DASH_KF = [
    { t: 0.0, parts: { BODY: { x: 0 } } },
    { t: 0.3, parts: { BODY: { x: 16 }, RIGHT_ARM: { x: 28, z: 8 }, LEFT_ARM: { x: 28, z: -8 }, HEAD: { x: -12 } } },
    { t: 0.7, parts: { BODY: { x: 10 }, RIGHT_ARM: { x: 16, z: 4 }, LEFT_ARM: { x: 16, z: -4 }, HEAD: { x: -8 } } },
    { t: 1.0, parts: { BODY: { x: 0 }, RIGHT_ARM: { x: 0, z: 0 }, LEFT_ARM: { x: 0, z: 0 }, HEAD: { x: 0 } } }
]
// arms flung wide (king's pulse, vortex, scream)
var SPREAD_KF = [
    { t: 0.0, parts: { RIGHT_ARM: { z: 0 }, LEFT_ARM: { z: 0 } } },
    { t: 0.3, parts: { RIGHT_ARM: { x: -20, z: 55 }, LEFT_ARM: { x: -20, z: -55 }, BODY: { x: -6 }, HEAD: { x: -12 } } },
    { t: 0.65, parts: { RIGHT_ARM: { x: -25, z: 60 }, LEFT_ARM: { x: -25, z: -60 }, BODY: { x: -6 }, HEAD: { x: -12 } } },
    { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, LEFT_ARM: { x: 0, z: 0 }, BODY: { x: 0 }, HEAD: { x: 0 } } }
]
// one-arm surgeon stance: hand raised palm-up, sweep out
var STANCE_KF = [
    { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
    { t: 0.3, parts: { RIGHT_ARM: { x: -70, y: -15 }, BODY: { y: -10 } } },
    { t: 0.7, parts: { RIGHT_ARM: { x: -60, y: 10 }, BODY: { y: 8 } } },
    { t: 1.0, parts: { RIGHT_ARM: { x: 0, y: 0 }, BODY: { y: 0 } } }
]
// iaijutsu draw from the left hip
var DRAW_KF = [
    { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
    { t: 0.3, parts: { RIGHT_ARM: { x: -25, z: -35 }, BODY: { y: -22 }, HEAD: { y: 10 } } },
    { t: 0.6, parts: { RIGHT_ARM: { x: -55, z: 25 }, BODY: { y: 18 }, HEAD: { y: -6 } } },
    { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, BODY: { y: 0 }, HEAD: { y: 0 } } }
]
// THOR: hammer thrust straight up to the sky, held while the storm answers,
// head tilted up to the heavens (god_of_thunder empower)
var THOR_RAISE_KF = [
    { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
    { t: 0.15, parts: { RIGHT_ARM: { x: -168, z: -6 }, HEAD: { x: -30 }, BODY: { x: -6 } } },
    { t: 0.85, parts: { RIGHT_ARM: { x: -168, z: -6 }, HEAD: { x: -30 }, BODY: { x: -6 } } },
    { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, HEAD: { x: 0 }, BODY: { x: 0 } } }
]
// THOR: smite - hammer raised overhead then snapped forward like a bolt (thunder_strike)
var THOR_POINT_KF = [
    { t: 0.0, parts: { RIGHT_ARM: { x: -150 }, HEAD: { x: -18 } } },
    { t: 0.4, parts: { RIGHT_ARM: { x: -150, z: 0 }, HEAD: { x: -18 }, BODY: { x: -8 } } },
    { t: 0.65, parts: { RIGHT_ARM: { x: -18, z: 0 }, BODY: { x: 18 }, HEAD: { x: 12 } } },
    { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, BODY: { x: 0 }, HEAD: { x: 0 } } }
]
// THOR: takeoff - hammer flung skyward, body arched back, legs trailing (storm_flight)
var THOR_FLY_KF = [
    { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
    { t: 0.2, parts: { RIGHT_ARM: { x: -172, z: -4 }, BODY: { x: -14 }, HEAD: { x: -24 }, RIGHT_LEG: { x: 22 }, LEFT_LEG: { x: -22 } } },
    { t: 0.8, parts: { RIGHT_ARM: { x: -172, z: -4 }, BODY: { x: -14 }, HEAD: { x: -24 }, RIGHT_LEG: { x: 22 }, LEFT_LEG: { x: -22 } } },
    { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, BODY: { x: 0 }, HEAD: { x: 0 }, RIGHT_LEG: { x: 0 }, LEFT_LEG: { x: 0 } } }
]

PalladiumEvents.registerAnimations((event) => {
    console.log('[Grand Line] registerAnimations v1')

    // PUNCH (priority 100)
    var PUNCHES = [
        ['grandline:gomu', 'pistol', 10],
        ['grandline:gomu_g2', 'jet_pistol', 8],
        ['grandline:gomu_g4', 'culverin', 8],
        ['grandline:mera', 'hiken', 10],
        ['grandline:ito', 'overheat', 8],
        ['grandline:gryphon_blade', 'divine_departure', 12],
        ['grandline:gryphon_blade', 'haki_slash', 8],
        ['grandline:mochi', 'mochi_fist', 8],
        ['grandline:gomu_g3', 'culverin', 8],
        ['grandline:loki_might', 'titan_fist', 8]
    ]
    event.register('gl_punch', 100, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < PUNCHES.length; i++) {
                var prog = getProgress(player, PUNCHES[i][0], PUNCHES[i][1], PUNCHES[i][2])
                if (prog >= 0) { applyKeyframes(builder, PUNCH_KF, prog); return }
            }
        } catch (e) {}
    })

    // SLAM (priority 110)
    var SLAMS = [
        ['grandline:gomu_g3', 'giant_pistol', 12],
        ['grandline:gomu_g3', 'giant_stomp', 10],
        ['grandline:hie', 'ice_age', 12],
        ['grandline:seiryu', 'thunder_bagua', 10],
        ['grandline:gura', 'seaquake', 20],
        ['grandline:ragnir_power', 'thunderclap', 12],
        ['grandline:loki_might', 'earth_shatter', 12]
    ]
    event.register('gl_slam', 110, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < SLAMS.length; i++) {
                var prog = getProgress(player, SLAMS[i][0], SLAMS[i][1], SLAMS[i][2])
                if (prog >= 0) { applyKeyframes(builder, SLAM_KF, prog); return }
            }
        } catch (e) {}
    })

    // DASH (priority 90)
    var DASHES = [
        ['grandline:gomu', 'rocket', 8],
        ['grandline:gomu_g2', 'jet_stamp', 8],
        ['grandline:phoenix', 'ascent', 10],
        ['grandline:phoenix', 'talon_dive', 8],
        ['grandline:pika', 'light_speed', 8],
        ['grandline:ito', 'sky_road', 8],
        ['grandline:ope', 'injection_shot', 8],
        ['grandline:ope', 'sterben', 8],
        ['grandline:mochi', 'buzz_cut', 10],
        ['grandline:gomu', 'bell_strike', 8],
        ['grandline:gomu_g4', 'bound_jump', 8],
        ['grandline:hie', 'ice_path', 8],
        ['grandline:pika', 'yata_mirror', 8],
        ['grandline:phoenix', 'solar_dive', 10],
        ['grandline:phoenix_form', 'ascent', 10],
        ['grandline:phoenix_form', 'talon_dive', 8],
        ['grandline:phoenix_form', 'solar_dive', 10]
    ]
    event.register('gl_dash', 90, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < DASHES.length; i++) {
                var prog = getProgress(player, DASHES[i][0], DASHES[i][1], DASHES[i][2])
                if (prog >= 0) { applyKeyframes(builder, DASH_KF, prog); return }
            }
        } catch (e) {}
    })

    // SPREAD (priority 120)
    var SPREADS = [
        ['grandline:ambition', 'kings_disposition', 16],
        ['grandline:emperor_presence', 'conquerors_pulse', 12],
        ['grandline:yami', 'black_hole', 16],
        ['grandline:soru', 'spirit_scream', 12],
        ['grandline:gomu_g5', 'joy_boost', 16],
        ['grandline:jiki', 'magnet_pull', 10],
        ['grandline:ragnir_power', 'chain_lightning', 12],
        ['grandline:pika', 'light_nova', 12],
        ['grandline:oden_two_heavens', 'boiled_defiance', 20],
        ['grandline:mochi', 'future_sight', 20],
        ['grandline:yami', 'liberation', 12],
        ['grandline:yami', 'kurouzu', 12],
        ['grandline:pika', 'blinding_flash', 8],
        ['grandline:mera', 'flame_ring', 10],
        ['grandline:jiki', 'repel', 8],
        ['grandline:jiki', 'item_vacuum', 10],
        ['grandline:hie', 'absolute_zero', 14],
        ['grandline:roger_will', 'kings_burst', 12],
        ['grandline:loki_might', 'giants_roar', 12],
        ['grandline:lucky_hat', 'crew_spirit', 12],
        ['grandline:sara', 'venom_cloud', 10],
        ['grandline:sara_form', 'venom_cloud', 10],
        ['grandline:phoenix', 'rebirth', 12],
        ['grandline:phoenix_form', 'rebirth', 12]
    ]
    event.register('gl_spread', 120, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < SPREADS.length; i++) {
                var prog = getProgress(player, SPREADS[i][0], SPREADS[i][1], SPREADS[i][2])
                if (prog >= 0) { applyKeyframes(builder, SPREAD_KF, prog); return }
            }
        } catch (e) {}
    })

    // SURGEON STANCE (priority 85): one-arm command pose (ROOM sweeps once per
    // toggle-enable; the rest are one-hand "control" moves - takt, parasite...)
    var STANCE_LIST = [
        ['grandline:ope', 'gamma_knife', 10],
        ['grandline:ope', 'shambles', 8],
        ['grandline:ope', 'takt', 8],
        ['grandline:mero', 'love_beam', 10],
        ['grandline:mochi', 'sticky_trap', 10],
        ['grandline:ito', 'parasite', 10],
        ['grandline:ito', 'parasite_bind', 10],
        ['grandline:soru', 'homies', 12],
        ['grandline:soru', 'lifespan_lend', 10],
        ['grandline:ambition', 'focus', 10],
        ['grandline:phoenix', 'regen_flames', 10],
        ['grandline:phoenix_form', 'regen_flames', 10],
        ['grandline:sara', 'regenerate', 10],
        ['grandline:sara_form', 'regenerate', 10]
    ]
    event.register('gl_stance', 85, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getToggleProgress(player, 'grandline:ope', 'room', 16)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, STANCE_KF, prog); return }
            for (var i = 0; i < STANCE_LIST.length; i++) {
                prog = getProgress(player, STANCE_LIST[i][0], STANCE_LIST[i][1], STANCE_LIST[i][2])
                if (prog >= 0) { applyKeyframes(builder, STANCE_KF, prog); return }
            }
        } catch (e) {}
    })

    // KIKOKU DRAW (priority 145): plays across the 20-tick draw activation
    event.register('gl_draw', 145, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getProgress(player, 'grandline:ope', 'draw_kikoku', 20)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, DRAW_KF, prog); return }
        } catch (e) {}
    })

    // LAW SHOULDER CARRY (priority 75): while Kikoku is held sheathed, the
    // right arm rises to steady the nodachi resting across the shoulder
    event.register('gl_carry', 75, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var held = ''
            try { held = '' + player.mainHandItem.id } catch (e) {}
            if (held != 'grandline:kikoku') return
            builder.get('RIGHT_ARM').setXRotDegrees(-42)
            builder.get('RIGHT_ARM').setZRotDegrees(-14)
        } catch (e) {}
    })

    // ZORO REST (priority 74): while his swords are sheathed he plants his
    // RIGHT hand down on the hilts at his RIGHT hip - the classic Zoro stance
    // (swords ride the right hip, so the right arm rests on them)
    event.register('gl_zoro_carry', 74, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var held = ''
            try { held = '' + player.mainHandItem.id } catch (e) {}
            if (held != 'grandline:zoro_katana') return
            builder.get('RIGHT_ARM').setXRotDegrees(-22)
            builder.get('RIGHT_ARM').setZRotDegrees(-24)
        } catch (e) {}
    })

    // ZORO CHARGE RUN (priority 150): during his running sword rushes he pitches
    // forward and strides hard - a proper charging-run pose (works even if not
    // pressing forward; the ability's Speed boost carries him)
    // arms held UP and SPREAD out to the sides (blades raised) while the legs
    // stride - the classic charging-swordsman pose (z spreads them off the body)
    var ZORO_RUN_KF = [
        { t: 0.0, parts: { BODY: { x: 0 } } },
        { t: 0.2, parts: { BODY: { x: -30 }, HEAD: { x: 20 }, RIGHT_ARM: { x: -120, z: -42 }, LEFT_ARM: { x: -120, z: 42 }, RIGHT_LEG: { x: -55 }, LEFT_LEG: { x: 48 } } },
        { t: 0.5, parts: { BODY: { x: -30 }, HEAD: { x: 20 }, RIGHT_ARM: { x: -128, z: -48 }, LEFT_ARM: { x: -128, z: 48 }, RIGHT_LEG: { x: 50 }, LEFT_LEG: { x: -55 } } },
        { t: 0.8, parts: { BODY: { x: -28 }, HEAD: { x: 18 }, RIGHT_ARM: { x: -120, z: -42 }, LEFT_ARM: { x: -120, z: 42 }, RIGHT_LEG: { x: -50 }, LEFT_LEG: { x: 45 } } },
        { t: 1.0, parts: { BODY: { x: 0 }, HEAD: { x: 0 }, RIGHT_ARM: { x: 0, z: 0 }, LEFT_ARM: { x: 0, z: 0 }, RIGHT_LEG: { x: 0 }, LEFT_LEG: { x: 0 } } }
    ]
    var ZORO_RUN_LIST = [ ['shishi', 10], ['toro_dash', 12], ['oni_giri', 12] ]
    event.register('gl_zoro_run', 150, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var zi = 0; zi < ZORO_RUN_LIST.length; zi++) {
                var prog = getProgress(player, 'grandline:zoro_blades', ZORO_RUN_LIST[zi][0], ZORO_RUN_LIST[zi][1])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, ZORO_RUN_KF, prog); return }
            }
        } catch (e) {}
    })

    // (The universal sword swing is now handled by BETTER COMBAT: the drawn
    //  blades are tagged into #bettercombat:katana / #bettercombat:sword via
    //  data/bettercombat/tags/items/, so they get the real multi-directional
    //  combo attack + ready pose. No manual stance here - that reverts Loki
    //  and Zoro from the earlier idle-pose experiment, as requested.)

    // SHANKS DRAW (priority 146): the Emperor raises his saber to the sky
    var SHANKS_DRAW_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
        { t: 0.35, parts: { RIGHT_ARM: { x: -150, z: 8 }, BODY: { x: -6 }, HEAD: { x: -18 } } },
        { t: 0.7, parts: { RIGHT_ARM: { x: -150, z: 8 }, BODY: { x: -6 }, HEAD: { x: -18 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, BODY: { x: 0 }, HEAD: { x: 0 } } }
    ]
    event.register('gl_draw2', 146, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getProgress(player, 'grandline:gryphon_blade', 'draw_gryphon', 20)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, SHANKS_DRAW_KF, prog); return }
        } catch (e) {}
    })

    // MIHAWK DRAW (priority 146): Yoru swept from the back down to guard
    var YORU_DRAW_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
        { t: 0.4, parts: { RIGHT_ARM: { x: -70, z: 18 }, BODY: { y: -14 }, HEAD: { y: 12 } } },
        { t: 0.75, parts: { RIGHT_ARM: { x: -35, z: 6 }, BODY: { y: 10 }, HEAD: { y: -6 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, BODY: { y: 0 }, HEAD: { y: 0 } } }
    ]
    event.register('gl_draw3', 146, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getProgress(player, 'grandline:yoru_blade', 'draw_yoru', 20)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, YORU_DRAW_KF, prog); return }
        } catch (e) {}
    })

    // ROGER DRAW (priority 146): the Pirate King pulls Ace from his hip in a
    // wide horizontal flourish
    var ACE_DRAW_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
        { t: 0.3, parts: { RIGHT_ARM: { x: -95, y: -28 }, BODY: { y: -16 }, HEAD: { y: 10 } } },
        { t: 0.65, parts: { RIGHT_ARM: { x: -85, y: 26 }, BODY: { y: 14 }, HEAD: { y: -8 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, y: 0 }, BODY: { y: 0 }, HEAD: { y: 0 } } }
    ]
    event.register('gl_draw4', 146, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getProgress(player, 'grandline:ace_blade', 'draw_ace', 20)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, ACE_DRAW_KF, prog); return }
        } catch (e) {}
    })

    // LUFFY IAI DRAW (priority 146): Nidai Kitetsu slides from the hip saya
    var NIDAI_DRAW_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
        { t: 0.35, parts: { RIGHT_ARM: { x: -55, y: -35 }, BODY: { y: -20 } } },
        { t: 0.7, parts: { RIGHT_ARM: { x: -80, y: 20 }, BODY: { y: 16 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, y: 0 }, BODY: { y: 0 } } }
    ]
    event.register('gl_draw6', 146, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getProgress(player, 'grandline:nidai_blade', 'draw_nidai', 20)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, NIDAI_DRAW_KF, prog); return }
        } catch (e) {}
    })

    // ODEN DRAW (priority 146): both blades pulled from the left-hip daisho
    var ODEN_DRAW_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 }, LEFT_ARM: { x: 0 } } },
        { t: 0.35, parts: { RIGHT_ARM: { x: -40, z: -30 }, LEFT_ARM: { x: -35, z: -20 }, BODY: { y: -24 }, HEAD: { y: 12 } } },
        { t: 0.7, parts: { RIGHT_ARM: { x: -70, z: 30 }, LEFT_ARM: { x: -65, z: -34 }, BODY: { y: 18 }, HEAD: { y: -8 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, LEFT_ARM: { x: 0, z: 0 }, BODY: { y: 0 }, HEAD: { y: 0 } } }
    ]
    event.register('gl_draw7', 146, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getProgress(player, 'grandline:oden_two_heavens', 'draw_oden', 20)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, ODEN_DRAW_KF, prog); return }
        } catch (e) {}
    })

    // BLACKBEARD GUN POSE (priority 147): the shooting arm snaps up straight
    // while any flintlock ability fires
    var GUN_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: -50 } } },
        { t: 0.2, parts: { RIGHT_ARM: { x: -90, z: -4 } } },
        { t: 0.8, parts: { RIGHT_ARM: { x: -90, z: -4 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 } } }
    ]
    var GUN_ABILITIES = [['gunshot', 6], ['volley', 10], ['pistol_whip', 6]]
    event.register('gl_gunpose', 152, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var gi = 0; gi < GUN_ABILITIES.length; gi++) {
                var prog = getProgress(player, 'grandline:flintlock', GUN_ABILITIES[gi][0], GUN_ABILITIES[gi][1])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, GUN_KF, prog); return }
            }
        } catch (e) {}
    })

    // THOR RAISE (priority 150): Ragnir thrust to the sky, calling the storm
    // down onto the hammer (god_of_thunder). Held through the 40-tick window.
    event.register('gl_thor_raise', 150, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getProgress(player, 'grandline:ragnir_power', 'god_of_thunder', 40)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, THOR_RAISE_KF, prog); return }
        } catch (e) {}
    })

    // THOR SMITE (priority 151): overhead-to-forward hammer strike (thunder_strike)
    event.register('gl_thor_point', 151, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getProgress(player, 'grandline:ragnir_power', 'thunder_strike', 12)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, THOR_POINT_KF, prog); return }
        } catch (e) {}
    })

    // THOR FLIGHT (priority 150): hammer flung skyward, body arched as the storm
    // lifts the wielder off the ground (storm_flight)
    event.register('gl_thor_fly', 150, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var prog = getProgress(player, 'grandline:ragnir_power', 'storm_flight', 40)
            if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, THOR_FLY_KF, prog); return }
        } catch (e) {}
    })

    // SWORD TRIPLE-SWING (priority 145): the basic three-swing combo played by
    // every blade's bread-and-butter slash ability
    var SWING3_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: -140, z: 10 }, BODY: { y: -10 } } },
        { t: 0.18, parts: { RIGHT_ARM: { x: -40, z: 26 }, BODY: { y: 12 } } },
        { t: 0.36, parts: { RIGHT_ARM: { x: -120, z: -24 }, BODY: { y: 14 } } },
        { t: 0.55, parts: { RIGHT_ARM: { x: -45, z: -30 }, BODY: { y: -12 } } },
        { t: 0.72, parts: { RIGHT_ARM: { x: -150, z: 0 }, BODY: { y: -4 } } },
        { t: 0.88, parts: { RIGHT_ARM: { x: -30, z: 6 }, BODY: { y: 8 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, BODY: { y: 0 } } }
    ]
    var SWING3_LIST = [
        ['grandline:gryphon_blade', 'haki_slash', 8],
        ['grandline:yoru_blade', 'worlds_strongest_slash', 12],
        ['grandline:ace_blade', 'supreme_slash', 8],
        ['grandline:nidai_blade', 'kitetsu_slash', 8],
        ['grandline:ragnir_power', 'quake_smash', 12],
        ['grandline:oden_two_heavens', 'flying_totsuka', 10],
        ['grandline:oden_two_heavens', 'nitoryu_kasumi', 8],
        ['grandline:pika', 'murakumo', 10],
        ['grandline:gryphon_blade', 'emperor_slash', 10],
        ['grandline:yoru_blade', 'black_cleave', 8],
        ['grandline:seiryu', 'wind_scythe', 10],
        ['grandline:seiryu_form', 'wind_scythe', 10],
        ['grandline:zoro_blades', 'iai_slash', 6]
    ]
    event.register('gl_swing3', 151, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var si = 0; si < SWING3_LIST.length; si++) {
                var prog = getProgress(player, SWING3_LIST[si][0], SWING3_LIST[si][1], SWING3_LIST[si][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, SWING3_KF, prog); return }
            }
        } catch (e) {}
    })

    // ================= v1.24.6 VARIETY PACK: distinct anime-style poses so a
    // character's abilities stop sharing one animation. All tracks end at 0.

    // BIG HAYMAKER (priority 101): huge wind-up overhead hook (Kong Gun tier)
    var BIGPUNCH_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
        { t: 0.3, parts: { RIGHT_ARM: { x: 35, y: -25, z: 10 }, BODY: { y: -28 }, HEAD: { y: 14 }, LEFT_ARM: { x: -15 } } },
        { t: 0.55, parts: { RIGHT_ARM: { x: -120, y: 15, z: -5 }, BODY: { y: 24, x: 6 }, LEFT_ARM: { x: 18 } } },
        { t: 0.8, parts: { RIGHT_ARM: { x: -50 }, BODY: { y: 8 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, y: 0, z: 0 }, BODY: { y: 0, x: 0 }, HEAD: { y: 0 }, LEFT_ARM: { x: 0 } } }
    ]
    var BIGPUNCH_LIST = [
        ['grandline:gomu_g4', 'kong_gun', 14],
        ['grandline:gomu_g5', 'bajrang_gun', 20],
        ['grandline:gura', 'quake_punch', 10],
        ['grandline:gryphon_blade', 'warlords_end', 12],
        ['grandline:jiki', 'assemble_cannon', 12]
    ]
    event.register('gl_bigpunch', 101, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < BIGPUNCH_LIST.length; i++) {
                var prog = getProgress(player, BIGPUNCH_LIST[i][0], BIGPUNCH_LIST[i][1], BIGPUNCH_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, BIGPUNCH_KF, prog); return }
            }
        } catch (e) {}
    })

    // RAPID FLURRY (priority 102): alternating machine-gun jabs (Gatling style)
    var RAPID_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
        { t: 0.15, parts: { RIGHT_ARM: { x: -85 }, LEFT_ARM: { x: -15 }, BODY: { y: -6 } } },
        { t: 0.3, parts: { RIGHT_ARM: { x: -15 }, LEFT_ARM: { x: -85 }, BODY: { y: 6 } } },
        { t: 0.45, parts: { RIGHT_ARM: { x: -85 }, LEFT_ARM: { x: -15 }, BODY: { y: -6 } } },
        { t: 0.6, parts: { RIGHT_ARM: { x: -15 }, LEFT_ARM: { x: -85 }, BODY: { y: 6 } } },
        { t: 0.8, parts: { RIGHT_ARM: { x: -40 }, LEFT_ARM: { x: -40 }, BODY: { y: 0 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0 }, LEFT_ARM: { x: 0 }, BODY: { y: 0 } } }
    ]
    var RAPID_LIST = [
        ['grandline:gomu', 'gatling', 20],
        ['grandline:jiki', 'punk_pistols', 10],
        ['grandline:mochi', 'yanagi_mochi', 14],
        ['grandline:sara', 'gill_lash', 10],
        ['grandline:sara_form', 'gill_lash', 10]
    ]
    event.register('gl_rapid', 102, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < RAPID_LIST.length; i++) {
                var prog = getProgress(player, RAPID_LIST[i][0], RAPID_LIST[i][1], RAPID_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, RAPID_KF, prog); return }
            }
        } catch (e) {}
    })

    // ADMIRAL KICK (priority 105): Kizaru's leg snaps up in a light-speed kick
    var KICK_KF = [
        { t: 0.0, parts: { RIGHT_LEG: { x: 0 } } },
        { t: 0.25, parts: { BODY: { x: -16 }, RIGHT_LEG: { x: -105 }, LEFT_ARM: { x: -35, z: -14 }, RIGHT_ARM: { x: 25, z: 10 }, HEAD: { x: 8 } } },
        { t: 0.55, parts: { BODY: { x: -10 }, RIGHT_LEG: { x: -70 }, LEFT_ARM: { x: -20 }, RIGHT_ARM: { x: 15 } } },
        { t: 1.0, parts: { BODY: { x: 0 }, RIGHT_LEG: { x: 0 }, LEFT_ARM: { x: 0, z: 0 }, RIGHT_ARM: { x: 0, z: 0 }, HEAD: { x: 0 } } }
    ]
    var KICK_LIST = [
        ['grandline:pika', 'light_kick', 8],
        ['grandline:mero', 'perfume_femur', 8]
    ]
    event.register('gl_kick', 105, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < KICK_LIST.length; i++) {
                var prog = getProgress(player, KICK_LIST[i][0], KICK_LIST[i][1], KICK_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, KICK_KF, prog); return }
            }
        } catch (e) {}
    })

    // CROSS SLASH (priority 152): both blades crossed high then RIPPED outward
    // in an X (Oden's Togen Totsuka / Paradise Totsuka)
    var XSLASH_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 }, LEFT_ARM: { x: 0 } } },
        { t: 0.25, parts: { RIGHT_ARM: { x: -155, z: -38 }, LEFT_ARM: { x: -155, z: 38 }, BODY: { x: -8 }, HEAD: { x: -8 } } },
        { t: 0.5, parts: { RIGHT_ARM: { x: -30, z: 42 }, LEFT_ARM: { x: -30, z: -42 }, BODY: { x: 16 }, HEAD: { x: 8 } } },
        { t: 0.75, parts: { RIGHT_ARM: { x: -15, z: 18 }, LEFT_ARM: { x: -15, z: -18 }, BODY: { x: 6 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, LEFT_ARM: { x: 0, z: 0 }, BODY: { x: 0 }, HEAD: { x: 0 } } }
    ]
    var XSLASH_LIST = [
        ['grandline:oden_two_heavens', 'togen_totsuka', 12],
        ['grandline:oden_two_heavens', 'paradise_totsuka', 16],
        ['grandline:zoro_blades', 'nigiri', 8],
        ['grandline:mera', 'cross_fire', 10],
        ['grandline:yoru_blade', 'sky_splitter', 12]
    ]
    event.register('gl_xslash', 152, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < XSLASH_LIST.length; i++) {
                var prog = getProgress(player, XSLASH_LIST[i][0], XSLASH_LIST[i][1], XSLASH_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, XSLASH_KF, prog); return }
            }
        } catch (e) {}
    })

    // WHIRLWIND SPIN (priority 152): full-body 360 with blades held wide
    // (Oden's Togen Shirataki)
    var SPIN_KF = [
        { t: 0.0, parts: { BODY: { y: 0 } } },
        { t: 0.35, parts: { BODY: { y: 170 }, RIGHT_ARM: { x: -20, z: 75 }, LEFT_ARM: { x: -20, z: -75 } } },
        { t: 0.7, parts: { BODY: { y: 330 }, RIGHT_ARM: { x: -20, z: 75 }, LEFT_ARM: { x: -20, z: -75 } } },
        { t: 0.9, parts: { BODY: { y: 355 }, RIGHT_ARM: { x: -8, z: 20 }, LEFT_ARM: { x: -8, z: -20 } } },
        { t: 1.0, parts: { BODY: { y: 360 }, RIGHT_ARM: { x: 0, z: 0 }, LEFT_ARM: { x: 0, z: 0 } } }
    ]
    var SPIN_LIST = [
        ['grandline:oden_two_heavens', 'togen_shirataki', 10],
        ['grandline:zoro_blades', 'tatsumaki', 14]
    ]
    event.register('gl_spin', 152, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < SPIN_LIST.length; i++) {
                var prog = getProgress(player, SPIN_LIST[i][0], SPIN_LIST[i][1], SPIN_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, SPIN_KF, prog); return }
            }
        } catch (e) {}
    })

    // SPEAR THRUST (priority 152): coil back then LUNGE the trident forward
    // (Katakuri's Mogura pierce)
    var THRUST_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
        { t: 0.3, parts: { RIGHT_ARM: { x: 35, y: 18 }, BODY: { y: -20 }, LEFT_ARM: { x: -20, z: -12 } } },
        { t: 0.55, parts: { RIGHT_ARM: { x: -88, y: -12 }, BODY: { y: 18, x: 10 }, HEAD: { y: -6 }, LEFT_ARM: { x: 22 } } },
        { t: 0.8, parts: { RIGHT_ARM: { x: -40 }, BODY: { y: 6 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, y: 0 }, BODY: { y: 0, x: 0 }, HEAD: { y: 0 }, LEFT_ARM: { x: 0, z: 0 } } }
    ]
    var THRUST_LIST = [
        ['grandline:mochi', 'mogura_pierce', 10],
        ['grandline:ace_blade', 'kamusari', 10],
        ['grandline:hie', 'partisan', 8],
        ['grandline:gura', 'air_crack', 10]
    ]
    event.register('gl_thrust', 152, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < THRUST_LIST.length; i++) {
                var prog = getProgress(player, THRUST_LIST[i][0], THRUST_LIST[i][1], THRUST_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, THRUST_KF, prog); return }
            }
        } catch (e) {}
    })

    // TWIN-PALM BEAM (priority 105): both hands thrust forward together and
    // HOLD while the light beam fires (Kizaru's Ama no Iwato)
    var BEAM_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 }, LEFT_ARM: { x: 0 } } },
        { t: 0.3, parts: { RIGHT_ARM: { x: -88, y: -22 }, LEFT_ARM: { x: -88, y: 22 }, BODY: { x: -6 }, HEAD: { x: -4 } } },
        { t: 0.75, parts: { RIGHT_ARM: { x: -92, y: -20 }, LEFT_ARM: { x: -92, y: 20 }, BODY: { x: -6 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, y: 0 }, LEFT_ARM: { x: 0, y: 0 }, BODY: { x: 0 }, HEAD: { x: 0 } } }
    ]
    var BEAM_LIST = [
        ['grandline:pika', 'grand_laser', 14],
        ['grandline:ope', 'counter_shock', 8],
        ['grandline:ope', 'countershock', 8],
        ['grandline:phoenix', 'azure_flare', 10],
        ['grandline:phoenix_form', 'azure_flare', 10]
    ]
    event.register('gl_beam', 105, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < BEAM_LIST.length; i++) {
                var prog = getProgress(player, BEAM_LIST[i][0], BEAM_LIST[i][1], BEAM_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, BEAM_KF, prog); return }
            }
        } catch (e) {}
    })

    // GROUND PUMMEL (priority 111): both fists hammer down twice
    // (Mochi Donuts / Whitebeard's Island Shaker)
    var PUMMEL_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 }, LEFT_ARM: { x: 0 } } },
        { t: 0.2, parts: { RIGHT_ARM: { x: -150 }, LEFT_ARM: { x: -150 }, BODY: { x: -10 } } },
        { t: 0.35, parts: { RIGHT_ARM: { x: -35 }, LEFT_ARM: { x: -35 }, BODY: { x: 14 } } },
        { t: 0.55, parts: { RIGHT_ARM: { x: -145 }, LEFT_ARM: { x: -145 }, BODY: { x: -8 } } },
        { t: 0.75, parts: { RIGHT_ARM: { x: -30 }, LEFT_ARM: { x: -30 }, BODY: { x: 12 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0 }, LEFT_ARM: { x: 0 }, BODY: { x: 0 } } }
    ]
    var PUMMEL_LIST = [
        ['grandline:mochi', 'mochi_donut', 12],
        ['grandline:gura', 'island_shaker', 24],
        ['grandline:sara', 'slime_slam', 10],
        ['grandline:sara_form', 'slime_slam', 10]
    ]
    event.register('gl_pummel', 111, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < PUMMEL_LIST.length; i++) {
                var prog = getProgress(player, PUMMEL_LIST[i][0], PUMMEL_LIST[i][1], PUMMEL_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, PUMMEL_KF, prog); return }
            }
        } catch (e) {}
    })

    // WIDE CLEAVE (priority 105): arms open T-wide then scythe inward crossed
    // (Katakuri's Zan Giri Mochi cutting arms)
    var WIDECLEAVE_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { z: 0 }, LEFT_ARM: { z: 0 } } },
        { t: 0.3, parts: { RIGHT_ARM: { x: -70, z: 80 }, LEFT_ARM: { x: -70, z: -80 }, BODY: { x: -6 } } },
        { t: 0.6, parts: { RIGHT_ARM: { x: -75, z: -30 }, LEFT_ARM: { x: -75, z: 30 }, BODY: { x: 10 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, z: 0 }, LEFT_ARM: { x: 0, z: 0 }, BODY: { x: 0 } } }
    ]
    var WIDECLEAVE_LIST = [
        ['grandline:mochi', 'zan_giri_mochi', 10],
        ['grandline:ace_blade', 'kings_cleave', 10],
        ['grandline:yoru_blade', 'crossed_slash', 10],
        ['grandline:zoro_blades', 'ashura', 12]
    ]
    event.register('gl_widecleave', 105, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < WIDECLEAVE_LIST.length; i++) {
                var prog = getProgress(player, WIDECLEAVE_LIST[i][0], WIDECLEAVE_LIST[i][1], WIDECLEAVE_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, WIDECLEAVE_KF, prog); return }
            }
        } catch (e) {}
    })

    // FINGER BARRAGE (priority 105): one arm locked forward, pulsing with each
    // shot (Kizaru's Yasakani no Magatama)
    var POINT_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 } } },
        { t: 0.2, parts: { RIGHT_ARM: { x: -92, y: -6 }, LEFT_ARM: { x: 8, z: 6 }, BODY: { y: -10 } } },
        { t: 0.4, parts: { RIGHT_ARM: { x: -84, y: -2 }, BODY: { y: -8 } } },
        { t: 0.6, parts: { RIGHT_ARM: { x: -92, y: -10 }, BODY: { y: -10 } } },
        { t: 0.8, parts: { RIGHT_ARM: { x: -88, y: -6 }, BODY: { y: -8 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, y: 0 }, LEFT_ARM: { x: 0, z: 0 }, BODY: { y: 0 } } }
    ]
    var POINT_LIST = [
        ['grandline:pika', 'yasakani', 10],
        ['grandline:mero', 'slave_arrow', 8],
        ['grandline:hie', 'pheasant_beak', 10],
        ['grandline:mera', 'firefly', 10],
        ['grandline:ito', 'tamaito', 8],
        ['grandline:zoro_blades', 'pound_ho', 10],
        ['grandline:nidai_blade', 'flying_cut', 10]
    ]
    event.register('gl_pointbarrage', 105, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < POINT_LIST.length; i++) {
                var prog = getProgress(player, POINT_LIST[i][0], POINT_LIST[i][1], POINT_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, POINT_KF, prog); return }
            }
        } catch (e) {}
    })

    // CONQUEROR'S GLARE (priority 95): chest out, fists clenched at the sides,
    // head rises into a slow domineering stare (intimidation / haki pressure)
    var GLARE_KF = [
        { t: 0.0, parts: { BODY: { x: 0 } } },
        { t: 0.25, parts: { BODY: { x: -8 }, HEAD: { x: -12 }, RIGHT_ARM: { x: 8, z: 18 }, LEFT_ARM: { x: 8, z: -18 } } },
        { t: 0.6, parts: { BODY: { x: -10 }, HEAD: { x: -16 }, RIGHT_ARM: { x: 10, z: 22 }, LEFT_ARM: { x: 10, z: -22 } } },
        { t: 1.0, parts: { BODY: { x: 0 }, HEAD: { x: 0 }, RIGHT_ARM: { x: 0, z: 0 }, LEFT_ARM: { x: 0, z: 0 } } }
    ]
    var GLARE_LIST = [
        ['grandline:emperor_presence', 'intimidation', 12],
        ['grandline:emperor_presence', 'dread_stare', 10],
        ['grandline:emperor_presence', 'kings_silence', 12],
        ['grandline:mihawk_presence', 'hawk_eye', 10],
        ['grandline:mihawk_presence', 'dark_authority', 12],
        ['grandline:mihawk_presence', 'predators_gaze', 10],
        ['grandline:mero', 'charm_aura', 10],
        ['grandline:roger_will', 'conquerors_presence', 12],
        ['grandline:soru', 'soul_pocus', 12],
        ['grandline:ambition', 'sixth_sense', 10],
        ['grandline:yami', 'dark_cloak', 10],
        ['grandline:yoru_blade', 'swordsmans_focus', 10]
    ]
    event.register('gl_glare', 95, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < GLARE_LIST.length; i++) {
                var prog = getProgress(player, GLARE_LIST[i][0], GLARE_LIST[i][1], GLARE_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, GLARE_KF, prog); return }
            }
        } catch (e) {}
    })

    // CROSS-GUARD PARRY (priority 112): both arms snap up crossed in front and
    // BRACE (parries, wards, iron will)
    var PARRY_KF = [
        { t: 0.0, parts: { RIGHT_ARM: { x: 0 }, LEFT_ARM: { x: 0 } } },
        { t: 0.2, parts: { RIGHT_ARM: { x: -70, y: -30, z: -15 }, LEFT_ARM: { x: -70, y: 30, z: 15 }, BODY: { x: 8 }, HEAD: { x: 6 } } },
        { t: 0.7, parts: { RIGHT_ARM: { x: -72, y: -28, z: -14 }, LEFT_ARM: { x: -72, y: 28, z: 14 }, BODY: { x: 8 }, HEAD: { x: 6 } } },
        { t: 1.0, parts: { RIGHT_ARM: { x: 0, y: 0, z: 0 }, LEFT_ARM: { x: 0, y: 0, z: 0 }, BODY: { x: 0 }, HEAD: { x: 0 } } }
    ]
    var PARRY_LIST = [
        ['grandline:ace_blade', 'captains_parry', 8],
        ['grandline:gryphon_blade', 'resolve_parry', 8],
        ['grandline:emperor_presence', 'sovereign_ward', 10],
        ['grandline:phoenix', 'phoenix_barrier', 10],
        ['grandline:phoenix_form', 'phoenix_barrier', 10],
        ['grandline:ambition', 'iron_will', 10],
        ['grandline:roger_will', 'laugh_tale_defiance', 16]
    ]
    event.register('gl_parry', 112, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < PARRY_LIST.length; i++) {
                var prog = getProgress(player, PARRY_LIST[i][0], PARRY_LIST[i][1], PARRY_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, PARRY_KF, prog); return }
            }
        } catch (e) {}
    })

    // DRAGON'S BREATH (priority 113): head rears back as the chest fills, then
    // whips forward to unleash the blast (Kaido's breath, toxic spit, balloon)
    var BREATH_KF = [
        { t: 0.0, parts: { HEAD: { x: 0 } } },
        { t: 0.3, parts: { HEAD: { x: -24 }, BODY: { x: -12 }, RIGHT_ARM: { x: 15, z: 25 }, LEFT_ARM: { x: 15, z: -25 } } },
        { t: 0.55, parts: { HEAD: { x: 14 }, BODY: { x: 10 }, RIGHT_ARM: { x: 8, z: 12 }, LEFT_ARM: { x: 8, z: -12 } } },
        { t: 0.8, parts: { HEAD: { x: 8 }, BODY: { x: 6 } } },
        { t: 1.0, parts: { HEAD: { x: 0 }, BODY: { x: 0 }, RIGHT_ARM: { x: 0, z: 0 }, LEFT_ARM: { x: 0, z: 0 } } }
    ]
    var BREATH_LIST = [
        ['grandline:seiryu', 'blast_breath', 12],
        ['grandline:seiryu_form', 'blast_breath', 12],
        ['grandline:seiryu', 'bolo_breath', 10],
        ['grandline:seiryu_form', 'bolo_breath', 10],
        ['grandline:sara', 'toxic_spit', 8],
        ['grandline:sara_form', 'toxic_spit', 8],
        ['grandline:gomu', 'balloon', 10]
    ]
    event.register('gl_breath', 113, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            for (var i = 0; i < BREATH_LIST.length; i++) {
                var prog = getProgress(player, BREATH_LIST[i][0], BREATH_LIST[i][1], BREATH_LIST[i][2])
                if (prog >= 0 && prog < 1.0) { applyKeyframes(builder, BREATH_KF, prog); return }
            }
        } catch (e) {}
    })

    // SHANKS ONE ARM (priority 60): wearing the Red Emperor's Cloak tucks the
    // left arm inside the cloak drape - the Emperor lost that arm to the sea
    event.register('gl_onearm', 60, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            var chest = ''
            try { chest = '' + player.chestArmorItem.id } catch (e) {}
            if (chest != 'grandline:red_emperor_cloak') return
            builder.get('LEFT_ARM').setXRotDegrees(8)
            builder.get('LEFT_ARM').setZRotDegrees(-24)
        } catch (e) {}
    })

    // PHOENIX GLIDE (priority 80): arms spread while airborne in phoenix form
    event.register('gl_glide', 80, (builder) => {
        try {
            if (!tpOnly(builder)) return
            var player = builder.getPlayer()
            if (!isActive(player, 'grandline:phoenix', 'phoenix_form')) return
            var airborne = false
            try { airborne = !player.isOnGround() } catch (e) { try { airborne = player.fallDistance > 0.5 } catch (e2) {} }
            if (!airborne) return
            builder.get('RIGHT_ARM').setZRotDegrees(70)
            builder.get('LEFT_ARM').setZRotDegrees(-70)
            builder.get('BODY').setXRotDegrees(8)
        } catch (e) {}
    })
})

console.log('[Grand Line] Client animation script v1 loaded')
