bobweb_8

trollium_V5promax

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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();
})();