bobweb_8

trollium_V5promax

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         bobweb_8
// @namespace    http://tampermonkey.net/
// @version      0.0.0.8
// @description  trollium_V5promax
// @author       bobweb_8
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    let uiVisible = true;
    const SPEED = 3, JUMP_FORCE = -10, GRAVITY = 0.5, GROUND = 300;
    const SWORD_RANGE = 120, DAGGER_RANGE = 60;
    const BEAM_DMG_INTERVAL = 30, CHARGE_NEEDED = 60, LUNGE_CHARGE_NEEDED = 180;
    const SHIELD_COOLDOWN_MAX = 900;
    const SWORD_CHARGE_MAX = 60;
    const FPS = 60;

    const st = {
        p1: { x: 100, y: GROUND, hp: 100, facing: 1 },
        p2: { x: 620, y: GROUND, hp: 100, facing: -1 },
        keys: {},
        // P1
        swordSwing: 0, swordCooldown: 0,
        swordCharge: 0, swordCharging: false, swordCharged: false,
        jumpHit: false, jumpHitAnim: 0, jumpHitCooldown: 0,
        groundSlamCooldown: 0,
        groundSlam: false, groundSlamAnim: 0,
        blocking: false, blockFlash: 0,
        shieldBroken: false, shieldCooldown: 0,
        shieldPushAnim: 0,
        bleeding: 0, bleedTimer: 0,
        p1Stunned: 0, // stun frames
        // P2
        daggerSwing: 0, daggerCooldown: 0,
        grappleCooldown: 0,
        grappleActive: false, grappleAnim: 0, // hook flying out
        grappleHooked: false, grapplePullAnim: 0, // hook landed, pulling p1
        grappleParryWindow: 0, // frames p2 can parry
        grappleX: 0, grappleY: 0, // hook tip position
        grappleStunDealt: false,
        // Combo state
        comboActive: false, comboStep: 0, comboTimer: 0,
        comboCooldown: 0,
        lungeCharge: 0, lunging: false, lungeAnim: 0,
        lungeStartX: 0, lungeEndX: 0,
        lungeFinisher: false, lungeFinisherAnim: 0,
        headX: 0, headY: 0, headVX: 0, headVY: 0,
        beamCharge: 0, beamDmgTimer: 0, beamActive: false,
        bullets: [],
        // Finisher
        jumpHitFinisher: false, jumpHitFinisherAnim: 0,
        p2TopHalfY: 0, p2TopVY: 0, p2TopVX: 0, p2BotY: 0,
        // Clash
        clashAnim: 0,
        gameOver: false, winner: '',
        particles: [],
        slamExplosionAnim: 0, slamExplosionX: 0
    };

    let vel = { p1: { vx: 0, vy: 0 }, p2: { vx: 0, vy: 0 } };

    const container = document.createElement('div');
    container.id = 'fighter-container';
    container.style.cssText = `position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:999999;font-family:monospace;user-select:none;`;
    const canvas = document.createElement('canvas');
    canvas.width = 750; canvas.height = 420;
    canvas.style.cssText = `display:block;border:3px solid #222;border-radius:8px;background:#1a1a2e;`;
    container.appendChild(canvas);
    document.body.appendChild(container);
    const ctx = canvas.getContext('2d');

    document.addEventListener('keydown', e => {
        const was = st.keys[e.code];
        st.keys[e.code] = true;

        if (e.key === 'RightShift') { uiVisible = !uiVisible; container.style.display = uiVisible ? 'block' : 'none'; }

        if (!st.gameOver) {
            // P1: Space
            if (e.code === 'Space' && !was) {
                e.preventDefault();
                if (st.p1Stunned > 0) { /* stunned, can't act */ }
                else if (st.blocking && !st.shieldBroken) { doShieldPush(); }
                else if (st.p1.y < GROUND && st.jumpHitCooldown <= 0) { doJumpHit(); }
                else if (st.swordCooldown <= 0) { st.swordCharging = true; st.swordCharge = 0; }
            }
            // P2: P = poison stab
            if (e.code === 'KeyP' && !was && st.daggerCooldown <= 0 && !st.p2Stunned) {
                doDaggerStab();
            }
            // P2: Enter = bullet tap
            if (e.code === 'Enter' && !was) { shootBullet(); }
            // P2: L = dagger tap OR combo continuation
            if (e.code === 'KeyL' && !was && !st.p2Stunned) {
                if (st.comboActive) {
                    advanceCombo();
                } else if (st.daggerCooldown <= 0 && st.lungeCharge === 0) {
                    st.daggerSwing = 10; st.daggerCooldown = 14;
                    doDaggerAttack();
                }
            }
            // P2: M = grapple hook
            if (e.code === 'KeyM' && !was && st.grappleCooldown <= 0 && !st.grappleActive && !st.grappleHooked) {
                fireGrapple();
            }
        }

        if (e.key === 'r' || e.key === 'R') resetGame();
        ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key) && e.preventDefault();
    });

    document.addEventListener('keyup', e => {
        st.keys[e.code] = false;
        if (e.code === 'Enter') { st.beamActive = false; st.beamDmgTimer = 0; }
        if (e.code === 'KeyL') {
            if (st.lungeCharge >= LUNGE_CHARGE_NEEDED && !st.gameOver) doLunge();
            st.lungeCharge = 0;
        }
        if (e.code === 'KeyF') st.blocking = false;
        if (e.code === 'Space' && st.swordCharging) releaseSwordSwing();
    });

    // ---- ACTIONS ----

    function releaseSwordSwing() {
        st.swordCharging = false;
        if (st.swordCooldown > 0) return;
        const charged = st.swordCharge >= SWORD_CHARGE_MAX;
        st.swordCharged = charged;
        st.swordSwing = charged ? 24 : 18;
        st.swordCooldown = charged ? 45 : 30;
        st.swordCharge = 0;
        const dx = (st.p2.x - st.p1.x) * st.p1.facing;
        if (dx > 0 && dx < SWORD_RANGE) {
            // Parry check: grapple hook p1 at start of swing
            if (st.grappleParryWindow > 0) {
                doParry();
                return;
            }
            checkClash(charged ? 9 : 1);
        }
    }

    function checkClash(dmg) {
        // Clash: if p2 is doing dagger swing at the same time and they're close
        const dist = Math.abs(st.p1.x - st.p2.x);
        if (st.daggerSwing > 5 && dist < DAGGER_RANGE + 20) {
            doClash();
        } else {
            dealDamage('p2', dmg, false);
            if (st.swordCharged) spawnSlashParticles(st.p2.x, st.p2.y - 40, '#ffffff');
        }
    }

    function doClash() {
        st.clashAnim = 30;
        // Both knocked back
        vel.p1.vx = -st.p1.facing * 8; vel.p1.vy = -4;
        vel.p2.vx = -st.p2.facing * 8; vel.p2.vy = -4;
        dealDamage('p1', 7, false, false, true); // ignoreKnockback flag
        dealDamage('p2', 7, false, false, true);
        for (let i = 0; i < 16; i++) {
            const a = (Math.PI*2/16)*i;
            state_spawnParticle(
                (st.p1.x+st.p2.x)/2, (st.p1.y+st.p2.y)/2 - 35,
                Math.cos(a)*5, Math.sin(a)*5,
                i%2===0?'#ffffff':'#ffdd44', 4+Math.random()*3, 25
            );
        }
    }

    function doJumpHit() {
        st.jumpHit = true; st.jumpHitAnim = 16; st.jumpHitCooldown = 90;
        const dx = (st.p2.x - st.p1.x) * st.p1.facing;
        if (dx > 0 && dx < SWORD_RANGE + 20) dealDamage('p2', 5, false, true);
        spawnSlashParticles(st.p1.x + st.p1.facing * 50, st.p1.y - 40, '#ffdd44');
    }

    function doShieldPush() {
        st.shieldPushAnim = 20;
        const dx = (st.p2.x - st.p1.x) * st.p1.facing;
        if (dx > 0 && dx < 80) {
            dealDamage('p2', 4, false);
            vel.p2.vx = st.p1.facing * 12; vel.p2.vy = -5;
        }
        spawnSlashParticles(st.p1.x + st.p1.facing * 30, st.p1.y - 35, '#88aaff');
    }

    function doDaggerAttack() {
        // Clash check
        const dist = Math.abs(st.p1.x - st.p2.x);
        if (st.swordSwing > 5 && dist < SWORD_RANGE) {
            doClash(); return;
        }
        const dx = (st.p1.x - st.p2.x) * st.p2.facing;
        if (dx > 0 && dx < DAGGER_RANGE) dealDamage('p1', 11, false);
    }

    function doDaggerStab() {
        st.daggerSwing = 12; st.daggerCooldown = 20;
        const dx = (st.p1.x - st.p2.x) * st.p2.facing;
        if (dx > 0 && dx < DAGGER_RANGE + 10) {
            dealDamage('p1', 11, false);
            st.bleeding = 5; st.bleedTimer = 60;
            spawnHitParticles(st.p1.x, st.p1.y - 40, '#cc0000');
        }
    }

    function shootBullet() {
        st.bullets.push({ x: st.p2.x+st.p2.facing*20, y: st.p2.y-50, vx: st.p2.facing*8, dir: st.p2.facing, reflected: false, life: 120 });
    }

    function fireGrapple() {
        st.grappleActive = true;
        st.grappleAnim = 0;
        st.grappleX = st.p2.x + st.p2.facing * 20;
        st.grappleY = st.p2.y - 45;
        st.grappleParryWindow = 12; // short window for parry
        st.grappleStunDealt = false;
    }

    function doGrappleHit() {
        // Hook lands on p1 — pull them in
        st.grappleActive = false;
        st.grappleHooked = true;
        st.grapplePullAnim = 25;
        spawnHitParticles(st.p1.x, st.p1.y - 40, '#44ffaa');
        // Stun p1 for 1 second (60 frames)
        st.p1Stunned = 60;
        // Unlock combo if last action was stab/grapple
        st.comboActive = true;
        st.comboStep = 0;
        st.comboTimer = 180; // 3 seconds to do combo
    }

    function advanceCombo() {
        if (!st.comboActive || st.comboTimer <= 0) return;
        const dist = Math.abs(st.p1.x - st.p2.x);
        if (dist > DAGGER_RANGE + 20) return; // must be close

        st.comboStep++;
        st.daggerSwing = 8;

        if (st.comboStep < 3) {
            // Stab 1 or 2
            dealDamage('p1', 8, false); // ~8*3 + kick = 34
            spawnSlashParticles(st.p1.x, st.p1.y - 40, '#ff6644');
        } else {
            // Final kick — combo complete
            dealDamage('p1', 10, false);
            vel.p1.vx = st.p2.facing * 12; vel.p1.vy = -6;
            spawnSlashParticles(st.p1.x, st.p1.y - 30, '#ffaa00');
            for (let i=0;i<10;i++) state_spawnParticle(st.p1.x,st.p1.y-30,(Math.random()-0.5)*6,-Math.random()*5,'#ffaa00',4,20);
            // Cooldowns on dagger and grapple
            st.daggerCooldown = Math.max(st.daggerCooldown, 15 * FPS / 10); // ~9s
            st.grappleCooldown = Math.max(st.grappleCooldown, 15 * FPS / 10);
            st.comboActive = false; st.comboStep = 0; st.comboTimer = 0;
            st.comboCooldown = 180;
        }
    }

    function doParry() {
        // P2 grappled p1 at start of P1 swing = parry! P1 stunned 2s
        st.p1Stunned = 120;
        st.grappleActive = false;
        st.grappleCooldown = 5 * FPS;
        vel.p1.vx = -st.p1.facing * 6; vel.p1.vy = -4;
        spawnShieldBreakParticles(st.p1.x, st.p1.y - 40);
        for (let i = 0; i < 8; i++) {
            const a = (Math.PI*2/8)*i;
            state_spawnParticle(st.p1.x,st.p1.y-40,Math.cos(a)*6,Math.sin(a)*6,'#ffaa00',5,30);
        }
    }

    function doLunge() {
        st.lungeStartX = st.p2.x;
        const landX = st.p1.x - st.p2.facing * 30;
        st.lungeEndX = Math.max(20, Math.min(canvas.width-20, landX));
        st.lunging = true; st.lungeAnim = 20;
    }

    function doGroundSlam(x) {
        st.groundSlam = true; st.groundSlamAnim = 30;
        st.slamExplosionX = x; st.slamExplosionAnim = 30;
        st.groundSlamCooldown = 10 * FPS;
        const dist = Math.abs(st.p2.x - x);
        if (dist < 160) dealDamage('p2', 7, false);
        for (let i=0;i<20;i++) {
            const a = Math.random()*Math.PI;
            state_spawnParticle(x,GROUND,Math.cos(a)*5,-Math.sin(a)*6,'#87CEEB',4+Math.random()*4,35);
            state_spawnParticle(x,GROUND,Math.cos(a)*3,-Math.sin(a)*4,'#ffffff',3+Math.random()*3,25);
        }
    }

    function dealDamage(target, amount, isLunge, isJumpHit=false, isClash=false) {
        if (target==='p1' && st.blocking && !st.shieldBroken && !isClash) {
            if (isLunge) {
                st.shieldBroken=true; st.shieldCooldown=SHIELD_COOLDOWN_MAX; st.blocking=false;
                spawnShieldBreakParticles(st.p1.x,st.p1.y-35);
                st.p1.hp=Math.max(0,st.p1.hp-17);
                spawnHitParticles(st.p1.x,st.p1.y-40,'#ff4444');
                if(st.p1.hp<=0) triggerLungeFinisher();
            } else { st.blockFlash=20; spawnBlockParticles(st.p1.x,st.p1.y-40); }
            return;
        }
        st[target].hp = Math.max(0, st[target].hp - amount);
        spawnHitParticles(st[target].x, st[target].y-40, target==='p1'?'#3498db':'#e74c3c');
        if (st[target].hp <= 0) {
            if (isLunge && target==='p1') { triggerLungeFinisher(); return; }
            if (target==='p2') { triggerJumpHitFinisher(); return; }
            st.gameOver=true; st.winner='Player 2';
        }
    }

    function triggerJumpHitFinisher() {
        st.jumpHitFinisher=true; st.jumpHitFinisherAnim=100;
        st.p2TopHalfY=st.p2.y-30; st.p2BotY=st.p2.y;
        st.p2TopVY=-9-Math.random()*3; st.p2TopVX=st.p1.facing*(2+Math.random()*2);
        for(let i=0;i<28;i++){const a=Math.random()*Math.PI*2,spd=3+Math.random()*6;state_spawnParticle(st.p2.x,st.p2.y-30,Math.cos(a)*spd,Math.sin(a)*spd-2,Math.random()<0.6?'#cc0000':'#ff4444',3+Math.random()*5,45);}
        for(let i=0;i<12;i++){const a=(Math.PI*2/12)*i;state_spawnParticle(st.p2.x,st.p2.y-30,Math.cos(a)*8,Math.sin(a)*8,'#ffffff',4,18);}
    }

    function triggerLungeFinisher() {
        st.lungeFinisher=true; st.lungeFinisherAnim=90;
        st.headX=st.p1.x; st.headY=st.p1.y-60;
        st.headVX=st.p2.facing*(3+Math.random()*2); st.headVY=-8-Math.random()*3;
        for(let i=0;i<30;i++){const a=Math.random()*Math.PI*2,spd=2+Math.random()*5;state_spawnParticle(st.p1.x,st.p1.y-60,Math.cos(a)*spd,Math.sin(a)*spd-3,Math.random()<0.7?'#cc0000':'#ff4444',3+Math.random()*5,40);}
        st.lungeEndX=Math.max(20,Math.min(canvas.width-20,st.p1.x+st.p2.facing*120)); st.lungeAnim=15;
    }

    function state_spawnParticle(x,y,vx,vy,color,size,life){st.particles.push({x,y,vx,vy,color,size,life,maxLife:life});}
    function spawnHitParticles(x,y,color){for(let i=0;i<12;i++){const a=(Math.PI*2/12)*i;state_spawnParticle(x,y,Math.cos(a)*(2+Math.random()*3),Math.sin(a)*(2+Math.random()*3),color,3+Math.random()*3,25);}}
    function spawnBlockParticles(x,y){for(let i=0;i<16;i++){const a=(Math.PI*2/16)*i;state_spawnParticle(x,y,Math.cos(a)*(1+Math.random()*4),Math.sin(a)*(1+Math.random()*4),'#ffdd44',2+Math.random()*3,20);}}
    function spawnShieldBreakParticles(x,y){for(let i=0;i<28;i++){const a=(Math.PI*2/28)*i,spd=2+Math.random()*6;state_spawnParticle(x,y,Math.cos(a)*spd,Math.sin(a)*spd-1,i%3===0?'#88aaff':i%3===1?'#ffffff':'#ffdd44',3+Math.random()*5,35);}}
    function spawnSlashParticles(x,y,color){for(let i=0;i<8;i++){const a=(Math.PI*2/8)*i;state_spawnParticle(x,y,Math.cos(a)*(3+Math.random()*3),Math.sin(a)*(3+Math.random()*3),color,3+Math.random()*4,20);}}
    function spawnLungeTrail(x,y){for(let i=0;i<6;i++){state_spawnParticle(x+(Math.random()-0.5)*10,y-20-Math.random()*40,(Math.random()-0.5)*2,-Math.random()*2,st.lungeFinisher?(Math.random()<0.5?'#cc0000':'#ff6600'):(i%2===0?'#aa44ff':'#ffffff'),3+Math.random()*4,18);}}
    function spawnChargeParticle(){const p2=st.p2,pct=st.beamCharge/CHARGE_NEEDED,a=Math.random()*Math.PI*2,r=30+Math.random()*40;const colors=['#ff2200','#ff6600','#ffaa00','#ff00aa','#ffffff'];state_spawnParticle(p2.x+p2.facing*20+Math.cos(a)*r,p2.y-55+Math.sin(a)*r,-Math.cos(a)*(1+pct*3),-Math.sin(a)*(1+pct*3),colors[Math.floor(Math.random()*colors.length)],2+Math.random()*4*pct,20);}

    function resetGame() {
        st.p1={x:100,y:GROUND,hp:100,facing:1}; st.p2={x:620,y:GROUND,hp:100,facing:-1};
        vel={p1:{vx:0,vy:0},p2:{vx:0,vy:0}};
        Object.assign(st,{
            swordSwing:0,swordCooldown:0,swordCharge:0,swordCharging:false,swordCharged:false,
            jumpHit:false,jumpHitAnim:0,jumpHitCooldown:0,groundSlamCooldown:0,
            groundSlam:false,groundSlamAnim:0,
            jumpHitFinisher:false,jumpHitFinisherAnim:0,p2TopHalfY:0,p2TopVY:0,p2TopVX:0,p2BotY:0,
            blocking:false,blockFlash:0,shieldBroken:false,shieldCooldown:0,shieldPushAnim:0,
            bleeding:0,bleedTimer:0,p1Stunned:0,
            daggerSwing:0,daggerCooldown:0,
            grappleCooldown:0,grappleActive:false,grappleAnim:0,grappleHooked:false,
            grapplePullAnim:0,grappleParryWindow:0,grappleX:0,grappleY:0,grappleStunDealt:false,
            comboActive:false,comboStep:0,comboTimer:0,comboCooldown:0,
            lungeCharge:0,lunging:false,lungeAnim:0,lungeStartX:0,lungeEndX:0,
            lungeFinisher:false,lungeFinisherAnim:0,headX:0,headY:0,headVX:0,headVY:0,
            beamCharge:0,beamDmgTimer:0,beamActive:false,bullets:[],
            clashAnim:0,gameOver:false,winner:'',particles:[],
            slamExplosionAnim:0,slamExplosionX:0
        });
    }

    // ---- UPDATE ----
    function update() {
        if (st.gameOver && !st.lungeFinisher && !st.jumpHitFinisher) return;

        // Stun
        if (st.p1Stunned > 0) st.p1Stunned--;
        if (st.clashAnim > 0) st.clashAnim--;

        // Shield CD
        if (st.shieldBroken) { st.shieldCooldown--; if(st.shieldCooldown<=0){st.shieldBroken=false;st.shieldCooldown=0;} }
        st.blocking = !!st.keys['KeyF'] && !st.shieldBroken && st.p1Stunned===0;
        if (st.blockFlash>0) st.blockFlash--;
        if (st.shieldPushAnim>0) st.shieldPushAnim--;

        // Bleed
        if (st.bleeding>0) {
            st.bleedTimer--;
            if (st.bleedTimer<=0) {
                st.bleedTimer=60; st.bleeding--;
                if(!st.gameOver){
                    st.p1.hp=Math.max(0,st.p1.hp-1);
                    state_spawnParticle(st.p1.x+(Math.random()-0.5)*20,st.p1.y-30-Math.random()*30,(Math.random()-0.5)*2,Math.random()*2,'#cc0000',3+Math.random()*3,25);
                    if(st.p1.hp<=0&&!st.gameOver){st.gameOver=true;st.winner='Player 2';}
                }
            }
        }

        // P1 movement (blocked if stunned)
        if (st.p1Stunned===0 && !st.blocking) {
            if (st.keys['KeyA']) { st.p1.x -= SPEED; st.p1.facing=-1; }
            if (st.keys['KeyD']) { st.p1.x += SPEED; st.p1.facing=1; }
        }
        if (st.keys['KeyW'] && st.p1.y>=GROUND && st.p1Stunned===0) vel.p1.vy=JUMP_FORCE;

        if (st.swordCharging && st.p1Stunned===0) {
            st.swordCharge=Math.min(st.swordCharge+1,SWORD_CHARGE_MAX);
            if(st.swordCharge>=SWORD_CHARGE_MAX) st.swordCharged=true;
        }

        // P2 movement
        if (!st.lunging) {
            if (st.keys['ArrowLeft'])  { st.p2.x-=SPEED; st.p2.facing=-1; }
            if (st.keys['ArrowRight']) { st.p2.x+=SPEED; st.p2.facing=1; }
            if (st.keys['ArrowUp']&&st.p2.y>=GROUND) vel.p2.vy=JUMP_FORCE;
        }

        // Knockback decay
        vel.p1.vx *= 0.8; vel.p2.vx *= 0.8;

        ['p1','p2'].forEach(p => {
            if (st.lungeFinisher && p==='p1') return;
            if (st.jumpHitFinisher && p==='p2') return;
            vel[p].vy += GRAVITY;
            st[p].y += vel[p].vy;
            st[p].x += vel[p].vx;
            if (st[p].y >= GROUND) {
                if (p==='p1' && vel.p1.vy>8 && st.jumpHit && st.groundSlamCooldown<=0) { doGroundSlam(st.p1.x); st.jumpHit=false; }
                st[p].y=GROUND; vel[p].vy=0;
            }
            st[p].x=Math.max(20,Math.min(canvas.width-20,st[p].x));
        });

        // Cooldowns
        if (st.swordSwing>0) st.swordSwing--;
        if (st.swordCooldown>0) st.swordCooldown--;
        if (st.jumpHitAnim>0) st.jumpHitAnim--;
        if (st.jumpHitCooldown>0) st.jumpHitCooldown--;
        if (st.groundSlamCooldown>0) st.groundSlamCooldown--;
        if (st.groundSlamAnim>0) st.groundSlamAnim--;
        if (st.slamExplosionAnim>0) st.slamExplosionAnim--;
        if (st.daggerSwing>0) st.daggerSwing--;
        if (st.daggerCooldown>0) st.daggerCooldown--;
        if (st.grappleCooldown>0) st.grappleCooldown--;
        if (st.grappleParryWindow>0) st.grappleParryWindow--;
        if (st.comboCooldown>0) st.comboCooldown--;
        if (st.comboTimer>0) { st.comboTimer--; if(st.comboTimer<=0){st.comboActive=false;st.comboStep=0;} }

        // Grapple hook flying
        if (st.grappleActive) {
            st.grappleAnim++;
            st.grappleX += st.p2.facing * 14;
            st.grappleY += 0;
            // Hit p1?
            if (Math.abs(st.grappleX - st.p1.x) < 25 && Math.abs(st.grappleY - (st.p1.y-40)) < 35) {
                // Parry check: if p1 was starting a sword swing
                if (st.swordSwing > 14) {
                    doParry();
                } else {
                    doGrappleHit();
                }
            }
            // Miss: hook too far
            if (st.grappleX < -30 || st.grappleX > canvas.width+30) {
                st.grappleActive=false;
                st.grappleCooldown=3*FPS;
            }
        }

        // Grapple pulling p1 in
        if (st.grappleHooked) {
            st.grapplePullAnim--;
            // Drag p1 toward p2
            const dx = st.p2.x - st.p1.x;
            st.p1.x += dx * 0.12;
            st.grappleX = st.p1.x; st.grappleY = st.p1.y - 40;
            if (st.grapplePullAnim <= 0) {
                st.grappleHooked = false;
                st.grappleCooldown = 3 * FPS; // 3s base cooldown after use
            }
        }

        // Lunge charge
        if (st.keys['KeyL'] && !st.lunging && st.lungeCharge < LUNGE_CHARGE_NEEDED) {
            st.lungeCharge++;
            const pct=st.lungeCharge/LUNGE_CHARGE_NEEDED;
            if(Math.random()<0.3+pct*0.5){const a=Math.random()*Math.PI*2,r=15+pct*25;state_spawnParticle(st.p2.x+Math.cos(a)*r,st.p2.y-30+Math.sin(a)*r,-Math.cos(a)*(1+pct*2),-Math.sin(a)*(1+pct*2)-0.5,pct<0.5?'#8844ff':pct<0.85?'#cc44ff':'#ffffff',2+Math.random()*3*pct,16);}
        }

        // Lunge anim
        if (st.lunging) {
            st.lungeAnim--;
            st.p2.x += (st.lungeEndX-st.p2.x)*0.45;
            spawnLungeTrail(st.p2.x,st.p2.y);
            if (st.lungeAnim===10&&!st.lungeFinisher) dealDamage('p1',13,true);
            if (st.lungeAnim<=0) { st.lunging=false; st.p2.x=st.lungeEndX; if(st.lungeFinisher){st.gameOver=true;st.winner='Player 2';} }
        }

        // Finisher head
        if (st.lungeFinisher) {
            st.lungeFinisherAnim--;
            st.headX+=st.headVX; st.headY+=st.headVY; st.headVY+=0.4;
            if(st.headY>GROUND+20){st.headVY*=-0.4;st.headY=GROUND+20;}
            if(st.lungeFinisherAnim>50&&Math.random()<0.4) state_spawnParticle(st.headX,st.headY+12,(Math.random()-0.5)*1,1+Math.random()*2,'#cc0000',2+Math.random()*3,20);
        }

        // Jump hit finisher halves
        if (st.jumpHitFinisher) {
            st.jumpHitFinisherAnim--;
            st.p2TopHalfY+=st.p2TopVY; st.p2TopVY+=0.45;
            st.p2.x+=st.p2TopVX; st.p2TopVX*=0.98;
            if(st.p2TopHalfY>GROUND-5){st.p2TopVY*=-0.35;st.p2TopHalfY=GROUND-5;}
            if(st.jumpHitFinisherAnim>40&&Math.random()<0.5){
                state_spawnParticle(st.p2.x+(Math.random()-0.5)*20,st.p2TopHalfY+20,(Math.random()-0.5)*1,1+Math.random()*2,'#cc0000',2+Math.random()*3,22);
                state_spawnParticle(st.p2.x+(Math.random()-0.5)*20,st.p2BotY-30,(Math.random()-0.5)*1,1+Math.random()*2,'#cc0000',2+Math.random()*3,22);
            }
            if(st.jumpHitFinisherAnim<=0){st.gameOver=true;st.winner='Player 1';}
        }

        // Bullets
        st.bullets=st.bullets.filter(b=>b.life>0&&b.x>-20&&b.x<canvas.width+20);
        for (const b of st.bullets) {
            b.x+=b.vx; b.life--;
            if (!b.reflected&&Math.abs(b.x-st.p1.x)<20&&Math.abs(b.y-(st.p1.y-45))<25) {
                if(st.shieldPushAnim>10){b.vx*=-1;b.dir*=-1;b.reflected=true;spawnSlashParticles(b.x,b.y,'#88aaff');}
                else{dealDamage('p1',11,false);b.life=0;}
            }
            if(b.reflected&&Math.abs(b.x-st.p2.x)<20&&Math.abs(b.y-(st.p2.y-45))<25){dealDamage('p2',1,false);b.life=0;}
        }

        // Beam
        if (st.keys['Enter']) {
            st.beamCharge=Math.min(st.beamCharge+1,CHARGE_NEEDED);
            if(st.beamCharge<CHARGE_NEEDED&&Math.random()<0.6+(st.beamCharge/CHARGE_NEEDED)*0.4) spawnChargeParticle();
            if(st.beamCharge>=CHARGE_NEEDED){
                st.beamActive=true; st.beamDmgTimer++;
                if(st.beamDmgTimer>=BEAM_DMG_INTERVAL){st.beamDmgTimer=0;if(isBeamHitting())dealDamage('p1',11,false);}
            }
        } else { st.beamActive=false; st.beamCharge=Math.max(0,st.beamCharge-2); st.beamDmgTimer=0; }

        st.particles=st.particles.filter(p=>p.life>0);
        st.particles.forEach(p=>{p.x+=p.vx;p.y+=p.vy;p.vy+=0.1;p.life--;});
    }

    function isBeamHitting(){return (st.p1.x-st.p2.x)*st.p2.facing>0;}

    // ---- DRAW ----

    function drawBeam() {
        if(!st.beamActive)return;
        const p2=st.p2,t=Date.now()/40,bx=p2.x+p2.facing*15,by=p2.y-55,endX=p2.facing>0?canvas.width+50:-50,dir=p2.facing,beamLen=Math.abs(endX-bx),startX=Math.min(bx,endX);
        ctx.save();
        for(let i=5;i>=1;i--){const w=20+i*18;ctx.globalAlpha=0.04+i*0.02;ctx.fillStyle=i%2===0?'#ff3300':'#ff8800';ctx.fillRect(startX,by-w/2,beamLen,w);}
        ctx.globalAlpha=0.55;ctx.fillStyle='#ff4400';ctx.fillRect(startX,by-22,beamLen,44);
        for(const l of [{w:28,c:'#cc2200',a:0.9},{w:18,c:'#ff5500',a:1},{w:10,c:'#ff9900',a:1},{w:5,c:'#ffee88',a:1},{w:2,c:'#ffffff',a:1}]){ctx.globalAlpha=l.a;ctx.fillStyle=l.c;ctx.fillRect(startX,by-l.w/2,beamLen,l.w);}
        ctx.globalAlpha=0.8;for(let i=0;i<20;i++){const off=((t*12*dir+i*38)%beamLen),px=dir>0?bx+off:bx-off,py=by+Math.sin(t+i*0.8)*7,sz=4+Math.sin(t*2+i)*3;ctx.fillStyle=i%3===0?'#ffffff':i%3===1?'#ffcc00':'#ff6600';ctx.beginPath();ctx.arc(px,py,sz,0,Math.PI*2);ctx.fill();}
        ctx.globalAlpha=0.6;for(let i=0;i<8;i++){const off=((t*10*dir+i*90)%beamLen),px=dir>0?bx+off:bx-off,sy=by+(i%2===0?-1:1)*(14+Math.sin(t+i)*8);ctx.strokeStyle='#ffaa00';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(px,by);ctx.lineTo(px+dir*5,sy);ctx.stroke();}
        ctx.globalAlpha=1;
        const pulse=1+Math.sin(t*3)*0.15,mR=22*pulse,grd=ctx.createRadialGradient(bx,by,0,bx,by,mR*2);
        grd.addColorStop(0,'rgba(255,255,255,1)');grd.addColorStop(0.3,'rgba(255,150,0,0.9)');grd.addColorStop(1,'rgba(255,50,0,0)');
        ctx.fillStyle=grd;ctx.beginPath();ctx.arc(bx,by,mR*2,0,Math.PI*2);ctx.fill();
        ctx.fillStyle='#ffaa00';for(let i=0;i<8;i++){const a=(Math.PI*2/8)*i+t*0.3,r1=mR*1.4,r2=mR*0.6;ctx.save();ctx.translate(bx,by);ctx.beginPath();ctx.moveTo(Math.cos(a)*r1,Math.sin(a)*r1);ctx.lineTo(Math.cos(a+0.3)*r2,Math.sin(a+0.3)*r2);ctx.lineTo(Math.cos(a+0.6)*r1*0.5,Math.sin(a+0.6)*r1*0.5);ctx.closePath();ctx.globalAlpha=0.7;ctx.fill();ctx.restore();}
        ctx.globalAlpha=1;ctx.restore();
    }

    function drawCharging() {
        const p2=st.p2,pct=st.beamCharge/CHARGE_NEEDED;if(pct<=0||st.beamActive)return;
        const cx=p2.x+p2.facing*20,cy=p2.y-55,t=Date.now()/60;
        for(let ring=0;ring<3;ring++){const r=12+ring*10+Math.sin(t+ring)*3;ctx.save();ctx.globalAlpha=pct*(0.5-ring*0.12);ctx.strokeStyle=ring===0?'#ff2200':ring===1?'#ff8800':'#ffff00';ctx.lineWidth=3-ring*0.5;ctx.beginPath();ctx.arc(cx,cy,r*pct,0,Math.PI*2*pct);ctx.stroke();ctx.restore();}
        const orbR=6+pct*10+Math.sin(t*4)*2*pct,grd=ctx.createRadialGradient(cx,cy,0,cx,cy,orbR*2);grd.addColorStop(0,`rgba(255,255,255,${pct})`);grd.addColorStop(0.4,`rgba(255,100,0,${pct*0.9})`);grd.addColorStop(1,`rgba(255,0,0,0)`);ctx.fillStyle=grd;ctx.beginPath();ctx.arc(cx,cy,orbR*2,0,Math.PI*2);ctx.fill();
        const nw=Math.floor(3+pct*5);for(let i=0;i<nw;i++){const a=(Math.PI*2/nw)*i+t*(1+pct),r=(18+pct*20)*pct;ctx.save();ctx.globalAlpha=pct*0.9;ctx.strokeStyle=i%2===0?'#ff4400':'#ffaa00';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(cx,cy);ctx.lineTo(cx+Math.cos(a)*r,cy+Math.sin(a)*r);ctx.stroke();ctx.beginPath();ctx.arc(cx+Math.cos(a)*r,cy+Math.sin(a)*r,3*pct,0,Math.PI*2);ctx.fillStyle='#ffcc00';ctx.fill();ctx.restore();}
        ctx.fillStyle='rgba(0,0,0,0.5)';ctx.fillRect(p2.x-22,p2.y-100,44,8);const bc=pct<0.5?'#ff6600':pct<0.85?'#ffaa00':'#00ffaa';ctx.fillStyle=bc;ctx.fillRect(p2.x-22,p2.y-100,44*pct,8);ctx.strokeStyle='#fff';ctx.lineWidth=0.5;ctx.strokeRect(p2.x-22,p2.y-100,44,8);if(pct>=0.99){ctx.fillStyle='#00ffaa';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.fillText('READY!',p2.x,p2.y-103);}
    }

    function drawLungeCharge() {
        if(st.lungeCharge<=0||st.lunging)return;
        const p2=st.p2,pct=st.lungeCharge/LUNGE_CHARGE_NEEDED,t=Date.now()/50,cx=p2.x,cy=p2.y-35;
        for(let ring=0;ring<2;ring++){const r=(16+ring*12)*pct;ctx.save();ctx.globalAlpha=pct*(0.7-ring*0.2);ctx.strokeStyle=ring===0?'#8844ff':'#dd88ff';ctx.lineWidth=3-ring;ctx.beginPath();ctx.arc(cx,cy,r,t*(ring===0?1:-0.7),t*(ring===0?1:-0.7)+Math.PI*2*pct);ctx.stroke();ctx.restore();}
        const orbR=4+pct*12+Math.sin(t*5)*2*pct,grd=ctx.createRadialGradient(cx,cy,0,cx,cy,orbR*2);grd.addColorStop(0,`rgba(255,255,255,${pct})`);grd.addColorStop(0.4,`rgba(180,50,255,${pct*0.9})`);grd.addColorStop(1,'rgba(100,0,200,0)');ctx.fillStyle=grd;ctx.beginPath();ctx.arc(cx,cy,orbR*2,0,Math.PI*2);ctx.fill();
        ctx.fillStyle='rgba(0,0,0,0.5)';ctx.fillRect(p2.x-22,p2.y-110,44,8);const bc=pct<0.6?'#8844ff':pct<0.9?'#cc44ff':'#ffffff';ctx.fillStyle=bc;ctx.fillRect(p2.x-22,p2.y-110,44*pct,8);ctx.strokeStyle='#cc88ff';ctx.lineWidth=0.5;ctx.strokeRect(p2.x-22,p2.y-110,44,8);if(pct>=0.99){ctx.fillStyle='#ffffff';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.fillText('LUNGE!',p2.x,p2.y-113);}
    }

    function drawSwordCharge() {
        if(!st.swordCharging)return;
        const pct=st.swordCharge/SWORD_CHARGE_MAX,t=Date.now()/50;
        const cx=st.p1.x+st.p1.facing*20,cy=st.p1.y-40;
        const grd=ctx.createRadialGradient(cx,cy,0,cx,cy,8+pct*12);grd.addColorStop(0,`rgba(255,255,255,${pct})`);grd.addColorStop(0.5,`rgba(100,200,255,${pct*0.8})`);grd.addColorStop(1,'rgba(50,100,200,0)');ctx.fillStyle=grd;ctx.beginPath();ctx.arc(cx,cy,8+pct*16,0,Math.PI*2);ctx.fill();
        if(pct>=1){ctx.strokeStyle='#ffffff';ctx.lineWidth=2;ctx.globalAlpha=0.5+Math.sin(t*8)*0.3;ctx.beginPath();ctx.arc(cx,cy,20,0,Math.PI*2);ctx.stroke();ctx.globalAlpha=1;}
        ctx.fillStyle='rgba(0,0,0,0.5)';ctx.fillRect(st.p1.x-22,st.p1.y-108,44,7);ctx.fillStyle=pct<0.6?'#4488ff':pct<0.9?'#88ddff':'#ffffff';ctx.fillRect(st.p1.x-22,st.p1.y-108,44*pct,7);ctx.strokeStyle='#88aaff';ctx.lineWidth=0.5;ctx.strokeRect(st.p1.x-22,st.p1.y-108,44,7);if(pct>=1){ctx.fillStyle='#ffffff';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.fillText('CHARGED!',st.p1.x,st.p1.y-112);}
    }

    function drawSlamExplosion() {
        if(st.slamExplosionAnim<=0)return;
        const pct=st.slamExplosionAnim/30,x=st.slamExplosionX,y=GROUND,r=(1-pct)*200;
        ctx.save();ctx.globalAlpha=pct*0.7;ctx.strokeStyle='#87CEEB';ctx.lineWidth=4;ctx.beginPath();ctx.arc(x,y,r,Math.PI,0);ctx.stroke();ctx.strokeStyle='#ffffff';ctx.lineWidth=2;ctx.beginPath();ctx.arc(x,y,r*0.7,Math.PI,0);ctx.stroke();
        ctx.globalAlpha=pct*0.4;const grd=ctx.createRadialGradient(x,y,0,x,y,r);grd.addColorStop(0,'rgba(180,220,255,0.8)');grd.addColorStop(1,'rgba(50,100,200,0)');ctx.fillStyle=grd;ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill();ctx.globalAlpha=1;ctx.restore();
    }

    function drawGrapple() {
        if (!st.grappleActive && !st.grappleHooked) return;
        const p2=st.p2;
        const originX=p2.x+p2.facing*18, originY=p2.y-45;
        // Rope
        ctx.save();
        ctx.strokeStyle='#44ffaa';ctx.lineWidth=2;ctx.globalAlpha=0.85;
        ctx.beginPath();ctx.moveTo(originX,originY);ctx.lineTo(st.grappleX,st.grappleY);ctx.stroke();
        // Hook tip
        ctx.fillStyle='#44ffaa';ctx.globalAlpha=1;
        ctx.beginPath();ctx.arc(st.grappleX,st.grappleY,5,0,Math.PI*2);ctx.fill();
        // Glow
        ctx.globalAlpha=0.3;const grd=ctx.createRadialGradient(st.grappleX,st.grappleY,0,st.grappleX,st.grappleY,12);grd.addColorStop(0,'rgba(68,255,170,0.8)');grd.addColorStop(1,'rgba(68,255,170,0)');ctx.fillStyle=grd;ctx.beginPath();ctx.arc(st.grappleX,st.grappleY,12,0,Math.PI*2);ctx.fill();
        ctx.globalAlpha=1;ctx.restore();
    }

    function drawClash() {
        if(st.clashAnim<=0)return;
        const pct=st.clashAnim/30,mx=(st.p1.x+st.p2.x)/2,my=(st.p1.y+st.p2.y)/2-35;
        ctx.save();ctx.globalAlpha=pct;
        const t=Date.now()/30;
        for(let i=0;i<8;i++){const a=(Math.PI*2/8)*i+t,r=20*(1-pct)+10;ctx.strokeStyle=i%2===0?'#ffffff':'#ffdd44';ctx.lineWidth=3;ctx.beginPath();ctx.moveTo(mx,my);ctx.lineTo(mx+Math.cos(a)*r,my+Math.sin(a)*r);ctx.stroke();}
        ctx.fillStyle='#ffffff';ctx.font=`bold ${Math.floor(18+pct*10)}px monospace`;ctx.textAlign='center';ctx.fillText('CLASH!',mx,my-20);
        ctx.globalAlpha=1;ctx.restore();
    }

    function drawParticles(){st.particles.forEach(p=>{ctx.globalAlpha=p.life/p.maxLife;ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,Math.max(0.1,p.size*(p.life/p.maxLife)),0,Math.PI*2);ctx.fill();});ctx.globalAlpha=1;}

    function drawSword(x,y,facing,swing,charged) {
        const t=swing/(charged?24:18),sa=facing>0?-Math.PI*0.8:Math.PI*1.8,ea=facing>0?Math.PI*0.15:Math.PI*0.85;
        const angle=sa+(ea-sa)*(1-t);
        ctx.save();ctx.translate(x+facing*10,y-35);ctx.rotate(angle);
        if(charged){ctx.shadowColor='#ffffff';ctx.shadowBlur=12;}
        ctx.fillStyle='#8B4513';ctx.fillRect(-3,0,6,18);ctx.fillStyle='#aaa';ctx.fillRect(-8,16,16,5);
        ctx.fillStyle=charged?'#ffffff':'#e8e8e8';ctx.fillRect(-2,20,4,35);
        ctx.beginPath();ctx.moveTo(-2,55);ctx.lineTo(2,55);ctx.lineTo(0,62);ctx.fillStyle='#fff';ctx.fill();
        if(t>0.3){ctx.globalAlpha=t*0.5;ctx.strokeStyle=charged?'#ffffff':'#87CEEB';ctx.lineWidth=charged?16:10;ctx.beginPath();ctx.arc(0,0,60,angle-facing*0.5,angle,facing<0);ctx.stroke();ctx.globalAlpha=1;}
        ctx.restore();
    }

    function drawDagger(x, y, facing, swing) {
        const maxSwing = swing <= 10 ? 10 : 12;
        const t = swing / maxSwing;
        const thrust = facing * Math.sin((1 - t) * Math.PI) * 28;
        ctx.save();
        ctx.translate(x + facing * 18 + thrust, y - 38);
        // Flip: tip points TOWARD player body (backward from facing)
        ctx.rotate(facing > 0 ? -Math.PI / 2 : Math.PI / 2);
        ctx.fillStyle='#5c3317';ctx.fillRect(-3,0,6,12);
        ctx.fillStyle='#888';ctx.fillRect(-7,11,14,4);
        ctx.fillStyle='#ddeeff';ctx.fillRect(-1.5,14,3,24);
        ctx.beginPath();ctx.moveTo(-1.5,38);ctx.lineTo(1.5,38);ctx.lineTo(0,45);ctx.fillStyle='#ffffff';ctx.fill();
        if(t<0.5){ctx.globalAlpha=(0.5-t)*1.4;ctx.strokeStyle='#aaddff';ctx.lineWidth=6;ctx.beginPath();ctx.moveTo(0,38);ctx.lineTo(0,56);ctx.stroke();ctx.globalAlpha=1;}
        ctx.restore();
    }

    function drawShield(x,y,facing,broken) {
        ctx.save();ctx.translate(x+facing*18,y-35);
        if(broken){ctx.globalAlpha=0.5;for(let side of[-1,1]){ctx.save();ctx.rotate(side*0.5);ctx.translate(side*5,side*3);ctx.fillStyle='#223366';ctx.beginPath();ctx.moveTo(side>0?0:-14,-18);ctx.lineTo(side>0?14:0,-10);ctx.lineTo(side>0?14:0,10);ctx.lineTo(side>0?0:-14,20);ctx.closePath();ctx.fill();ctx.strokeStyle='#4466aa';ctx.lineWidth=2;ctx.stroke();ctx.restore();}ctx.restore();return;}
        if(st.blockFlash>0){ctx.globalAlpha=st.blockFlash/20*0.6;ctx.fillStyle='#ffdd44';ctx.beginPath();ctx.arc(0,0,22,0,Math.PI*2);ctx.fill();ctx.globalAlpha=1;}
        if(st.shieldPushAnim>0){const push=(st.shieldPushAnim/20)*8;ctx.translate(facing*push,0);}
        ctx.fillStyle='#4477cc';ctx.beginPath();ctx.moveTo(0,-18);ctx.lineTo(14,-10);ctx.lineTo(14,10);ctx.lineTo(0,20);ctx.lineTo(-14,10);ctx.lineTo(-14,-10);ctx.closePath();ctx.fill();
        ctx.strokeStyle='#88aaff';ctx.lineWidth=2;ctx.stroke();ctx.fillStyle='#88aaff';ctx.fillRect(-2,-10,4,20);ctx.fillRect(-8,-2,16,4);
        ctx.restore();
    }

    function drawHeadChopped(){
        if(!st.lungeFinisher)return;
        ctx.save();ctx.translate(st.headX,st.headY);ctx.rotate((st.headX-st.p1.x)*0.08);
        ctx.fillStyle='#3498db';ctx.beginPath();ctx.arc(0,0,12,0,Math.PI*2);ctx.fill();
        ctx.strokeStyle='#fff';ctx.lineWidth=2;for(let s of[-1,1]){ctx.beginPath();ctx.moveTo(s*4-2,-4);ctx.lineTo(s*4+2,0);ctx.stroke();ctx.beginPath();ctx.moveTo(s*4+2,-4);ctx.lineTo(s*4-2,0);ctx.stroke();}
        ctx.fillStyle='#cc0000';ctx.fillRect(-6,10,12,5);ctx.restore();
    }

    function drawStunIndicator(x, y, frames) {
        if (frames <= 0) return;
        const t = Date.now() / 200;
        ctx.save();
        for (let i = 0; i < 3; i++) {
            const a = (Math.PI * 2 / 3) * i + t;
            const sx = x + Math.cos(a) * 16, sy = y - 85 + Math.sin(a) * 5;
            ctx.fillStyle = '#ffdd44';
            ctx.font = '14px monospace';
            ctx.textAlign = 'center';
            ctx.fillText('★', sx, sy);
        }
        const secs = Math.ceil(frames / FPS);
        ctx.fillStyle = '#ffdd44'; ctx.font = 'bold 10px monospace';
        ctx.fillText(`STUNNED ${secs}s`, x, y - 92);
        ctx.restore();
    }

    function drawPlayer(p, color, label) {
        const x=st[p].x,y=st[p].y,f=st[p].facing;

        if (p==='p1') {
            if(st.lungeFinisher){
                ctx.fillStyle=color;ctx.fillRect(x-15,y-50,30,40);ctx.fillStyle='#cc0000';ctx.fillRect(x-6,y-52,12,6);ctx.fillStyle=color;ctx.fillRect(x-12,y-10,10,20);ctx.fillRect(x+2,y-10,10,20);
                return;
            }
            if(st.blocking||st.shieldBroken) drawShield(x,y,f,st.shieldBroken);
            else if(st.swordSwing>0) drawSword(x,y,f,st.swordSwing,st.swordCharged);
            else {
                ctx.save();ctx.translate(x+f*10,y-35);ctx.rotate(f>0?-Math.PI*0.6:Math.PI*1.6);
                if(st.swordCharging&&st.swordCharge>30){ctx.shadowColor='#87CEEB';ctx.shadowBlur=8;}
                ctx.fillStyle='#8B4513';ctx.fillRect(-3,0,6,18);ctx.fillStyle='#aaa';ctx.fillRect(-8,16,16,5);
                ctx.fillStyle=st.swordCharge>=SWORD_CHARGE_MAX?'#ffffff':'#e8e8e8';ctx.fillRect(-2,20,4,35);
                ctx.fillStyle='#fff';ctx.beginPath();ctx.moveTo(-2,55);ctx.lineTo(2,55);ctx.lineTo(0,62);ctx.fill();ctx.restore();
            }
        }

        if (p==='p2') {
            if(st.jumpHitFinisher){
                const bx=st.p2.x,botY=st.p2BotY,topY=st.p2TopHalfY,tumble=(100-st.jumpHitFinisherAnim)*0.04;
                ctx.fillStyle='#e74c3c';ctx.fillRect(bx-15,botY-25,30,25);ctx.fillRect(bx-12,botY-2,10,18);ctx.fillRect(bx+2,botY-2,10,18);ctx.fillStyle='#cc0000';ctx.fillRect(bx-8,botY-27,16,5);
                ctx.save();ctx.translate(bx,topY);ctx.rotate(tumble*f);ctx.fillStyle='#e74c3c';ctx.fillRect(-15,-25,30,25);ctx.beginPath();ctx.arc(0,-35,12,0,Math.PI*2);ctx.fill();
                ctx.fillStyle='#fff';ctx.fillRect(f*3-3,-39,6,5);ctx.fillStyle='#000';ctx.fillRect(f*4-2,-38,3,3);
                ctx.strokeStyle='#fff';ctx.lineWidth=2;for(let s of[-1,1]){ctx.beginPath();ctx.moveTo(s*4-2,-39);ctx.lineTo(s*4+2,-35);ctx.stroke();ctx.beginPath();ctx.moveTo(s*4+2,-39);ctx.lineTo(s*4-2,-35);ctx.stroke();}
                ctx.fillStyle='#cc0000';ctx.fillRect(-8,-1,16,5);ctx.restore();
                return;
            }
            if(st.daggerSwing>0) drawDagger(x,y,f,st.daggerSwing);
            else {
                ctx.save();ctx.translate(x+f*18,y-30);ctx.rotate(f>0?-Math.PI/2:Math.PI/2);
                ctx.fillStyle='#5c3317';ctx.fillRect(-3,0,6,12);ctx.fillStyle='#888';ctx.fillRect(-7,11,14,4);ctx.fillStyle='#ddeeff';ctx.fillRect(-1.5,14,3,22);
                ctx.beginPath();ctx.moveTo(-1.5,36);ctx.lineTo(1.5,36);ctx.lineTo(0,42);ctx.fillStyle='#ffffff';ctx.fill();ctx.restore();
            }
        }

        if(p==='p1'&&st.lungeFinisher)return;
        if(p==='p2'&&st.jumpHitFinisher)return;
        if(p==='p2'&&st.lunging){ctx.save();ctx.globalAlpha=0.35;ctx.fillStyle='#aa44ff';ctx.fillRect(x-15,y-50,30,40);ctx.beginPath();ctx.arc(x,y-60,12,0,Math.PI*2);ctx.fill();ctx.restore();}

        if(p==='p1'&&st.bleeding>0){ctx.save();ctx.globalAlpha=0.3;ctx.fillStyle='#cc0000';ctx.fillRect(x-15,y-50,30,40);ctx.beginPath();ctx.arc(x,y-60,12,0,Math.PI*2);ctx.fill();ctx.restore();}
        if(p==='p1'&&st.jumpHitAnim>0){ctx.save();ctx.globalAlpha=st.jumpHitAnim/16*0.5;ctx.fillStyle='#ffdd44';ctx.fillRect(x-15,y-50,30,40);ctx.beginPath();ctx.arc(x,y-60,12,0,Math.PI*2);ctx.fill();ctx.restore();}
        // Stun flash
        if(p==='p1'&&st.p1Stunned>0){ctx.save();ctx.globalAlpha=0.25+Math.sin(Date.now()/60)*0.15;ctx.fillStyle='#ffdd44';ctx.fillRect(x-15,y-50,30,40);ctx.beginPath();ctx.arc(x,y-60,12,0,Math.PI*2);ctx.fill();ctx.restore();}
        // Combo active flash on p2
        if(p==='p2'&&st.comboActive&&st.comboStep<3){ctx.save();ctx.globalAlpha=0.2+Math.sin(Date.now()/80)*0.15;ctx.fillStyle='#ff6644';ctx.fillRect(x-15,y-50,30,40);ctx.beginPath();ctx.arc(x,y-60,12,0,Math.PI*2);ctx.fill();ctx.restore();}

        ctx.fillStyle=color;ctx.fillRect(x-15,y-50,30,40);ctx.beginPath();ctx.arc(x,y-60,12,0,Math.PI*2);ctx.fill();
        ctx.fillStyle='#fff';ctx.fillRect(x+f*3-3,y-64,6,5);ctx.fillStyle='#000';ctx.fillRect(x+f*4-2,y-63,3,3);
        ctx.fillStyle=color;ctx.fillRect(x-12,y-10,10,20);ctx.fillRect(x+2,y-10,10,20);
        if(p==='p1'&&st.blocking&&!st.shieldBroken){ctx.globalAlpha=0.25;ctx.fillStyle='#88aaff';ctx.fillRect(x-15,y-50,30,40);ctx.beginPath();ctx.arc(x,y-60,12,0,Math.PI*2);ctx.fill();ctx.globalAlpha=1;}
        ctx.fillStyle='#fff';ctx.font='bold 12px monospace';ctx.textAlign='center';ctx.fillText(label,x,y-76);
    }

    function drawHPBar(x,y,hp,color,label){
        ctx.fillStyle='#333';ctx.fillRect(x,y,200,18);ctx.fillStyle=hp>30?color:'#e74c3c';ctx.fillRect(x,y,hp*2,18);ctx.strokeStyle='rgba(255,255,255,0.4)';ctx.lineWidth=1;ctx.strokeRect(x,y,200,18);ctx.fillStyle='#fff';ctx.font='bold 12px monospace';ctx.textAlign='left';ctx.fillText(`${label}: ${hp}/100`,x+4,y+13);
    }

    function drawCooldownBar(x, y, cur, max, color) {
        if (cur <= 0) return;
        const pct = 1 - cur / max;
        ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.fillRect(x, y, 80, 5);
        ctx.fillStyle = color; ctx.fillRect(x, y, 80 * pct, 5);
        ctx.strokeStyle = color; ctx.lineWidth = 0.5; ctx.strokeRect(x, y, 80, 5);
    }

    function draw() {
        ctx.clearRect(0,0,canvas.width,canvas.height);
        ctx.fillStyle='#1a1a2e';ctx.fillRect(0,0,canvas.width,canvas.height);
        ctx.fillStyle='#4a4a6a';ctx.fillRect(0,GROUND+10,canvas.width,canvas.height-GROUND-10);
        ctx.fillStyle='#6a6a9a';ctx.fillRect(0,GROUND+8,canvas.width,4);

        drawSlamExplosion();
        drawBeam();drawCharging();drawLungeCharge();drawSwordCharge();
        drawGrapple();
        drawBullets();
        drawParticles();
        drawPlayer('p1','#3498db','P1');
        drawPlayer('p2','#e74c3c','P2');
        drawHeadChopped();
        drawClash();
        drawStunIndicator(st.p1.x, st.p1.y, st.p1Stunned);

        drawHPBar(20,15,st.p1.hp,'#3498db','P1');
        drawHPBar(530,15,st.p2.hp,'#e74c3c','P2');

        // P1 status
        let p1y=40;
        drawCooldownBar(20,p1y,st.swordCooldown,45,'rgba(100,180,255,0.6)');p1y+=8;
        drawCooldownBar(20,p1y,st.jumpHitCooldown,90,'rgba(255,220,50,0.6)');p1y+=8;
        drawCooldownBar(20,p1y,st.groundSlamCooldown,10*FPS,'rgba(135,206,235,0.6)');p1y+=8;
        if(st.blocking){ctx.fillStyle='#88aaff';ctx.font='bold 11px monospace';ctx.textAlign='left';ctx.fillText('BLOCKING',20,p1y+8);p1y+=14;}
        if(st.shieldBroken){const s=Math.ceil(st.shieldCooldown/60),pct=1-st.shieldCooldown/SHIELD_COOLDOWN_MAX;ctx.fillStyle='#ff4444';ctx.font='bold 11px monospace';ctx.textAlign='left';ctx.fillText(`SHIELD BROKEN (${s}s)`,20,p1y+8);p1y+=12;drawCooldownBar(20,p1y,st.shieldCooldown,SHIELD_COOLDOWN_MAX,'rgba(100,100,255,0.5)');p1y+=8;}
        if(st.p1Stunned>0){ctx.fillStyle='#ffdd44';ctx.font='bold 11px monospace';ctx.textAlign='left';ctx.fillText(`STUNNED ${Math.ceil(st.p1Stunned/FPS)}s`,20,p1y+8);p1y+=14;}
        if(st.bleeding>0){ctx.fillStyle='#cc4444';ctx.font='bold 11px monospace';ctx.textAlign='left';ctx.fillText(`BLEEDING ${st.bleeding}s`,20,p1y+8);}

        // P2 status
        let p2y=40;
        drawCooldownBar(530,p2y,st.daggerCooldown,20,'rgba(255,150,150,0.6)');p2y+=8;
        drawCooldownBar(530,p2y,st.grappleCooldown,3*FPS,'rgba(68,255,170,0.6)');p2y+=8;
        drawCooldownBar(530,p2y,st.comboCooldown,180,'rgba(255,100,50,0.6)');p2y+=8;
        if(st.comboActive){ctx.fillStyle='#ff6644';ctx.font='bold 11px monospace';ctx.textAlign='right';ctx.fillText(`COMBO ${st.comboStep}/3 — press L!`,canvas.width-16,p2y+8);p2y+=14;}
        if(st.lunging){ctx.fillStyle='#cc44ff';ctx.font='bold 11px monospace';ctx.textAlign='right';ctx.fillText('LUNGING!',canvas.width-16,p2y+8);p2y+=14;}
        if(st.beamActive){ctx.fillStyle='#ff6600';ctx.font='bold 11px monospace';ctx.textAlign='right';ctx.fillText('BEAM FIRING',canvas.width-16,p2y+8);}

        if(st.lungeFinisher&&st.lungeFinisherAnim>60){ctx.save();ctx.globalAlpha=Math.min(1,(st.lungeFinisherAnim-60)/20);ctx.fillStyle='#ff2200';ctx.font='bold 28px monospace';ctx.textAlign='center';ctx.fillText('LUNGE FINISHER',canvas.width/2,90);ctx.restore();}
        if(st.jumpHitFinisher&&st.jumpHitFinisherAnim>70){ctx.save();ctx.globalAlpha=Math.min(1,(st.jumpHitFinisherAnim-70)/20);ctx.fillStyle='#ffdd00';ctx.font='bold 28px monospace';ctx.textAlign='center';ctx.fillText('CLEAVE FINISHER',canvas.width/2,90);ctx.restore();}

        ctx.fillStyle='rgba(255,255,255,0.22)';ctx.font='9px monospace';ctx.textAlign='center';
        ctx.fillText('P1: WASD+Space(swing/charge/jumpHit/shieldPush)+F(block)  |  P2: Arrows+L(dagger/lunge)+P(poison)+M(grapple)+Enter(bullet/beam)',canvas.width/2,canvas.height-5);

        if(st.gameOver){
            ctx.fillStyle='rgba(0,0,0,0.65)';ctx.fillRect(0,0,canvas.width,canvas.height);
            if(st.lungeFinisher){drawParticles();drawHeadChopped();}
            if(st.jumpHitFinisher){drawParticles();drawPlayer('p2','#e74c3c','P2');}
            ctx.fillStyle=st.lungeFinisher?'#ff2200':st.jumpHitFinisher?'#ffdd00':'#f1c40f';
            ctx.font='bold 48px monospace';ctx.textAlign='center';ctx.fillText(`${st.winner} Wins!`,canvas.width/2,canvas.height/2-20);
            if(st.lungeFinisher){ctx.fillStyle='#ff8888';ctx.font='bold 20px monospace';ctx.fillText('EXECUTION',canvas.width/2,canvas.height/2+15);}
            if(st.jumpHitFinisher){ctx.fillStyle='#ffee88';ctx.font='bold 20px monospace';ctx.fillText('CLEAVED IN HALF',canvas.width/2,canvas.height/2+15);}
            ctx.fillStyle='#fff';ctx.font='20px monospace';ctx.fillText('Press R to play again',canvas.width/2,canvas.height/2+45);
        }
    }

    function drawBullets(){for(const b of st.bullets){ctx.save();ctx.fillStyle=b.reflected?'#88aaff':'#ffcc00';if(b.reflected){ctx.shadowColor='#88aaff';ctx.shadowBlur=8;}ctx.beginPath();ctx.arc(b.x,b.y,6,0,Math.PI*2);ctx.fill();ctx.fillStyle='#ffffff';ctx.beginPath();ctx.arc(b.x-b.dir*2,b.y,3,0,Math.PI*2);ctx.fill();ctx.restore();}}

    function loop() { update(); draw(); requestAnimationFrame(loop); }
    loop();
})();