// renderPlayers.js — Rendu des joueurs (sprites Kael ou cercles colorés)

import { Container, Graphics, Text, Assets, Sprite } from 'pixi.js';
import { CLASS_COLORS, PLAYER_HITBOX_RADIUS, KAEL_MELEE_RADIUS, KAEL_SLAM_RADIUS, KAEL_STORM_RADIUS,
         DISPLACEMENT_COOLDOWNS } from './constants.js';
import { iso, getTh, getTw } from './renderer.js';

let _debugAttackRange = false;
export function setDebugAttackRange(v) { _debugAttackRange = v; }

let _debugHitboxes = false;
export function setDebugHitboxes(v) { _debugHitboxes = v; }

const BUFF_TINTS = {
    invulnerable: 0xffcc44,
    casting:      0x44ffff,
    flying:       0x88ccff,
    intangible:   0xcc88ff,
};

// Assets Kael
const _kt = {};
let _kaelReady = false;

// Assets Seris
const _st = {};
let _serisReady = false;

// Assets Aldric
const _at = {};
let _aldricReady = false;

export async function loadKaelAssets() {
    if (_kaelReady) return;
    const b = '../assets/sprites/kael/';

    // Génère les entrées [clé, chemin] pour une animation
    // prefix = 'run', 'atk', 'dash', 'sk2', 'sk3'
    // dirs   = tableau de directions (ex: ['south','north','east','west'])
    // n      = nombre de frames
    // folder = dossier dans animations/
    function anim(prefix, dirs, n, folder) {
        const out = [];
        for (const d of dirs) {
            for (let i = 0; i < n; i++) {
                const pad = String(i).padStart(3, '0');
                out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
            }
        }
        return out;
    }

    const D4 = ['south', 'north', 'east', 'west'];
    const D1 = ['south'];

    const entries = [
        // Rotations statiques (idle)
        ['south', b + 'rotations/south.png'],
        ['north', b + 'rotations/north.png'],
        ['east',  b + 'rotations/east.png'],
        ['west',  b + 'rotations/west.png'],
        // Animations
        ...anim('run',  D4, 4, 'running'),   // running 4 dirs × 4 frames
        ...anim('atk',  D4, 5, 'attack'),    // attaque  4 dirs × 5 frames
        ...anim('dash', D4, 5, 'dash'),      // dash     4 dirs × 5 frames
        ...anim('sk2',  D1, 9, 'skill2'),    // frappe lourde south × 9 frames
        ...anim('sk3',  D1,17, 'skill3'),    // tempête      south × 17 frames
    ];

    for (const [key, path] of entries) {
        const tex = await Assets.load(path);
        tex.source.scaleMode = 'nearest';
        _kt[key] = tex;
    }
    _kaelReady = true;
}

export async function loadSerisAssets() {
    if (_serisReady) return;
    const b = '../assets/sprites/seris/';

    function anim(prefix, dirs, n, folder) {
        const out = [];
        for (const d of dirs) {
            for (let i = 0; i < n; i++) {
                const pad = String(i).padStart(3, '0');
                out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
            }
        }
        return out;
    }

    const D4 = ['south', 'north', 'east', 'west'];
    const D1 = ['south'];

    const entries = [
        ['s_south', b + 'rotations/south.png'],
        ['s_north', b + 'rotations/north.png'],
        ['s_east',  b + 'rotations/east.png'],
        ['s_west',  b + 'rotations/west.png'],
        ...anim('srun',  D4, 6, 'running'),   // course 4 dirs × 6 frames
        ...anim('satk',  D4, 7, 'attack'),    // attaque 4 dirs × 7 frames
        ...anim('stele', D1, 5, 'teleport'),  // téléport south × 5 frames
        ...anim('ssk1',  D1, 9, 'skill1'),    // éventail south × 9 frames
        ...anim('ssk2',  D1,13, 'skill2'),    // vide     south × 13 frames
    ];

    for (const [key, path] of entries) {
        const tex = await Assets.load(path);
        tex.source.scaleMode = 'nearest';
        _st[key] = tex;
    }
    _serisReady = true;
}


export async function loadAldricAssets() {
    if (_aldricReady) return;
    const b = '../assets/sprites/aldric/';

    function anim(prefix, dirs, n, folder) {
        const out = [];
        for (const d of dirs) {
            for (let i = 0; i < n; i++) {
                const pad = String(i).padStart(3, '0');
                out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
            }
        }
        return out;
    }

    const D4 = ['south', 'north', 'east', 'west'];
    const D1 = ['south'];

    const entries = [
        ['a_south', b + 'rotations/south.png'],
        ['a_north', b + 'rotations/north.png'],
        ['a_east',  b + 'rotations/east.png'],
        ['a_west',  b + 'rotations/west.png'],
        ...anim('awk',  D4, 6, 'walking'),  // marche 4 dirs × 6 frames
        ...anim('aatk', D4, 7, 'attack'),   // attaque 4 dirs × 7 frames
        ...anim('afly', D1, 9, 'fly'),      // envol south × 9 frames
        ...anim('ask1', D1, 9, 'skill1'),   // halo south × 9 frames
        ...anim('ask2', D1,13, 'skill2'),   // vague south × 13 frames
    ];

    for (const [key, path] of entries) {
        const tex = await Assets.load(path);
        tex.source.scaleMode = 'nearest';
        _at[key] = tex;
    }
    _aldricReady = true;
}


// Constantes d'animation
const TICK_DT    = 0.05;   // durée d'un tick serveur (20 Hz)

const RUN_FPS    = 8;   const RUN_TICK  = 1 / RUN_FPS;
const ATK_FPS    = 14;  const ATK_TICK  = 1 / ATK_FPS;  const ATK_FRAMES = 5;
const DASH_FPS   = 14;  const DASH_TICK = 1 / DASH_FPS; const DASH_FRAMES = 5;
const DASH_DUR   = DASH_FRAMES / DASH_FPS;   // ~0.36s — durée totale du dash visuel

const SK2_FPS    = 12;  const SK2_TICK  = 1 / SK2_FPS;  const SK2_FRAMES = 9;
const SK3_FPS    = 10;  const SK3_TICK  = 1 / SK3_FPS;  const SK3_FRAMES = 17;

// Cooldowns max des skills (pour détecter le déclenchement côté client)
// Si le CD passe de ~0 à une valeur > THRESHOLD, le skill vient d'être utilisé.
const DISP_CD_MAX  = 9.0;
const SK2_CD_MAX   = 7.0;    // kael slam (ability_1)
const SK3_CD_MAX   = 12.0;   // kael tempête (ability_2)

// Seris animation frame counts + FPS
const S_RUN_FRAMES  = 6;
const S_ATK_FRAMES  = 7;
const S_TELE_FRAMES = 5;
const S_SK1_FRAMES  = 9;
const S_SK2_FRAMES  = 13;
const S_RUN_FPS     = 10;  const S_RUN_TICK  = 1 / S_RUN_FPS;
const S_ATK_FPS     = 14;  const S_ATK_TICK  = 1 / S_ATK_FPS;
const S_TELE_FPS    = 14;  const S_TELE_TICK = 1 / S_TELE_FPS;
const S_SK1_FPS     = 12;  const S_SK1_TICK  = 1 / S_SK1_FPS;
const S_SK2_FPS     = 10;  const S_SK2_TICK  = 1 / S_SK2_FPS;

// Cooldowns Seris pour détection côté client
const S_DISP_CD_MAX = 4.0;   // téléport
const S_SK1_CD_MAX  = 6.0;   // éventail
const S_SK2_CD_MAX  = 10.0;  // vide

// Aldric animation frame counts + FPS
const A_WK_FRAMES   = 6;
const A_ATK_FRAMES  = 7;
const A_FLY_FRAMES  = 9;
const A_SK1_FRAMES  = 9;
const A_SK2_FRAMES  = 13;
const A_WK_FPS      = 8;   const A_WK_TICK  = 1 / A_WK_FPS;
const A_ATK_FPS     = 12;  const A_ATK_TICK = 1 / A_ATK_FPS;
const A_FLY_FPS     = 12;  const A_FLY_TICK = 1 / A_FLY_FPS;
const A_SK1_FPS     = 10;  const A_SK1_TICK = 1 / A_SK1_FPS;
const A_SK2_FPS     = 10;  const A_SK2_TICK = 1 / A_SK2_FPS;

// Cooldowns Aldric pour détection côté client
const A_DISP_CD_MAX = 14.0;  // envol
const A_SK1_CD_MAX  = 8.0;   // halo
const A_SK2_CD_MAX  = 15.0;  // vague

// État d'animation par joueur
const _prevPos     = {};   // position précédente (pour détecter direction et dash)
const _animState   = {};   // état d'animation complet
const _prevAtkCd   = {};   // cooldown attaque au tick précédent
const _prevDispCd  = {};   // cooldown displacement au tick précédent
const _prevAb2Cd   = {};   // cooldown ability_2 au tick précédent
const _prevAb3Cd   = {};   // cooldown ability_3 au tick précédent
const _dashAnim    = {};   // { fromX, fromY, toX, toY, t } — interpolation de position dash


// Mise à jour

export function updatePlayers(layer, pool, players) {
    const ids = new Set(players.map(p => p.id));

    // Supprimer les joueurs déconnectés
    for (const [id, container] of Object.entries(pool)) {
        if (!ids.has(id)) {
            layer.removeChild(container);
            delete pool[id];
            delete _prevPos[id]; delete _animState[id];
            delete _prevAtkCd[id]; delete _prevDispCd[id];
            delete _prevAb2Cd[id]; delete _prevAb3Cd[id];
            delete _dashAnim[id];
        }
    }

    for (const p of players) {
        // Création du sprite si nouveau joueur
        if (!pool[p.id]) {
            pool[p.id] = _createPlayerGraphic(p);
            layer.addChild(pool[p.id]);
            _prevPos[p.id]    = { x: p.x, y: p.y };
            _animState[p.id]  = {
                facing: 'south',
                frame: 0, timer: 0,
                attacking: false, atkFrame: 0, atkTimer: 0,
                dashing:  false, dashDir: 'south', dashFrame: 0, dashTimer: 0,
                sk2ing:   false, sk2Frame: 0, sk2Timer: 0,
                sk3ing:   false, sk3Frame: 0, sk3Timer: 0,
                // Seris
                steling: false, steleFrame: 0, steleTimer: 0,
                ssk1ing: false, ssk1Frame: 0, ssk1Timer: 0,
                ssk2ing: false, ssk2Frame: 0, ssk2Timer: 0,
                // Aldric
                aflying: false, aflyFrame: 0, aflyTimer: 0,
                ask1ing: false, ask1Frame: 0, ask1Timer: 0,
                ask2ing: false, ask2Frame: 0, ask2Timer: 0,
            };
            _prevAtkCd[p.id]  = 0;
            _prevDispCd[p.id] = 0;
            _prevAb2Cd[p.id]  = 0;
            _prevAb3Cd[p.id]  = 0;
        }

        const prev   = _prevPos[p.id];
        const dx     = p.x - prev.x;
        const dy     = p.y - prev.y;
        const moving = Math.abs(dx) > 0.01 || Math.abs(dy) > 0.01;
        const anim   = _animState[p.id];

        // Détecter les déclenchements d'actions
        // Attaque de base : attack CD 0 → > 0
        const curAtkCd = p.cooldowns?.attack ?? 0;
        if (curAtkCd > 0 && _prevAtkCd[p.id] === 0) {
            anim.attacking = true; anim.atkFrame = 0; anim.atkTimer = 0;
        }
        _prevAtkCd[p.id] = curAtkCd;

        // Displacement (E) — seuil par classe
        const dispCdMax = DISPLACEMENT_COOLDOWNS[p.class] ?? DISP_CD_MAX;
        const curDispCd = p.cooldowns?.displacement ?? 0;
        if (curDispCd > _prevDispCd[p.id] + dispCdMax * 0.5) {
            if (p.class === 'kael') {
                anim.dashing   = true;
                anim.dashDir   = anim.facing;
                anim.dashFrame = 0;
                anim.dashTimer = 0;
                _dashAnim[p.id] = {
                    fromX: prev.x, fromY: prev.y,
                    toX: p.x, toY: p.y,
                    t: 0,
                };
            } else if (p.class === 'seris') {
                anim.steling   = true;
                anim.steleFrame = 0;
                anim.steleTimer = 0;
            } else if (p.class === 'aldric') {
                anim.aflying   = true;
                anim.aflyFrame = 0;
                anim.aflyTimer = 0;
            }
        }
        _prevDispCd[p.id] = curDispCd;

        // Skill 1
        const curAb2Cd = p.cooldowns?.ability_1 ?? 0;
        if (p.class === 'kael') {
            if (curAb2Cd > _prevAb2Cd[p.id] + SK2_CD_MAX * 0.5) {
                anim.sk2ing = true; anim.sk2Frame = 0; anim.sk2Timer = 0;
            }
        } else if (p.class === 'seris') {
            if (curAb2Cd > _prevAb2Cd[p.id] + S_SK1_CD_MAX * 0.5) {
                anim.ssk1ing = true; anim.ssk1Frame = 0; anim.ssk1Timer = 0;
            }
        } else if (p.class === 'aldric') {
            if (curAb2Cd > _prevAb2Cd[p.id] + A_SK1_CD_MAX * 0.5) {
                anim.ask1ing = true; anim.ask1Frame = 0; anim.ask1Timer = 0;
            }
        }
        _prevAb2Cd[p.id] = curAb2Cd;

        // Skill 2
        const curAb3Cd = p.cooldowns?.ability_2 ?? 0;
        if (p.class === 'kael') {
            if (curAb3Cd > _prevAb3Cd[p.id] + SK3_CD_MAX * 0.5) {
                anim.sk3ing = true; anim.sk3Frame = 0; anim.sk3Timer = 0;
            }
        } else if (p.class === 'seris') {
            if (curAb3Cd > _prevAb3Cd[p.id] + S_SK2_CD_MAX * 0.5) {
                anim.ssk2ing = true; anim.ssk2Frame = 0; anim.ssk2Timer = 0;
            }
        } else if (p.class === 'aldric') {
            if (curAb3Cd > _prevAb3Cd[p.id] + A_SK2_CD_MAX * 0.5) {
                anim.ask2ing = true; anim.ask2Frame = 0; anim.ask2Timer = 0;
            }
        }
        _prevAb3Cd[p.id] = curAb3Cd;

        // Mise à jour direction depuis le mouvement
        if (moving && !anim.dashing) {
            anim.facing = _detectDir(dx, dy) || anim.facing;
        }

        // Calcul de la position à afficher
        // Pendant un dash : interpoler linéairement de fromPos à toPos
        let renderX = p.x, renderY = p.y;
        const da = _dashAnim[p.id];
        if (da) {
            da.t += TICK_DT;
            if (da.t >= DASH_DUR) {
                delete _dashAnim[p.id];   // dash terminé
            } else {
                const alpha = da.t / DASH_DUR;
                renderX = da.fromX + (da.toX - da.fromX) * alpha;
                renderY = da.fromY + (da.toY - da.fromY) * alpha;
            }
        }
        const pos = iso(renderX, renderY);

        // Choisir la texture
        if (p.class === 'kael' && _kaelReady && pool[p.id]._sprite) {
            _updateKaelSprite(pool[p.id]._sprite, anim, moving);
        } else if (p.class === 'seris' && _serisReady && pool[p.id]._sprite) {
            _updateSerisSprite(pool[p.id]._sprite, anim, moving);
        } else if (p.class === 'aldric' && _aldricReady && pool[p.id]._sprite) {
            _updateAldricSprite(pool[p.id]._sprite, anim, moving);
        }

        // Debug : zones d'attaque
        if (p.class === 'kael' && pool[p.id]._rangeGraphic) {
            const rg = pool[p.id]._rangeGraphic;
            rg.clear();
            if (_debugAttackRange) {
                const tw  = getTw();
                const th2 = getTh();
                if (anim.attacking) {
                    // Attaque de base — demi-cercle (on affiche un cercle complet pour simplifier)
                    const rx = KAEL_MELEE_RADIUS * tw / 2;
                    const ry = KAEL_MELEE_RADIUS * th2 / 2;
                    rg.ellipse(0, 0, rx, ry).fill({ color: 0xff4444, alpha: 0.18 });
                    rg.ellipse(0, 0, rx, ry).stroke({ color: 0xff6666, width: 1.5, alpha: 0.7 });
                }
                if (anim.sk2ing) {
                    // Skill 2 — slam cercle complet (orange)
                    const rx = KAEL_SLAM_RADIUS * tw / 2;
                    const ry = KAEL_SLAM_RADIUS * th2 / 2;
                    rg.ellipse(0, 0, rx, ry).fill({ color: 0xff8800, alpha: 0.18 });
                    rg.ellipse(0, 0, rx, ry).stroke({ color: 0xffaa44, width: 1.5, alpha: 0.8 });
                }
                if (anim.sk3ing) {
                    // Skill 3 — tempête cercle large (violet)
                    const rx = KAEL_STORM_RADIUS * tw / 2;
                    const ry = KAEL_STORM_RADIUS * th2 / 2;
                    rg.ellipse(0, 0, rx, ry).fill({ color: 0xaa44ff, alpha: 0.15 });
                    rg.ellipse(0, 0, rx, ry).stroke({ color: 0xcc88ff, width: 2, alpha: 0.8 });
                }
            }
        }

        // Debug : hitbox de collision du joueur
        if (pool[p.id]._hboxG) {
            const hg = pool[p.id]._hboxG;
            hg.clear();
            if (_debugHitboxes) {
                const rx = PLAYER_HITBOX_RADIUS * getTw() / 2;
                const ry = PLAYER_HITBOX_RADIUS * getTh() / 2;
                hg.ellipse(0, 0, rx, ry).stroke({ color: 0x00ffff, width: 1.5, alpha: 0.9 });
            }
        }

        _prevPos[p.id] = { x: p.x, y: p.y };

        // Teinte selon buff
        let tint  = 0xffffff;
        let alpha = 1.0;
        for (const b of (p.buffs ?? [])) {
            if (BUFF_TINTS[b.type]) { tint = BUFF_TINTS[b.type]; break; }
        }
        if (p.buffs?.some(b => b.type === 'intangible')) alpha = 0.5;

        pool[p.id].x     = pos.x;
        pool[p.id].y     = pos.y;
        pool[p.id].tint  = tint;
        pool[p.id].alpha = p.alive ? alpha : 0.2;
    }
}


// Logique de sprite Kael

function _updateKaelSprite(sprite, anim, moving) {
    // Priorité : dash > tempête (sk3) > frappe lourde (sk2) > attaque > run > idle

    if (anim.dashing) {
        anim.dashTimer += TICK_DT;
        if (anim.dashTimer >= DASH_TICK) {
            anim.dashTimer -= DASH_TICK;
            anim.dashFrame++;
        }
        if (anim.dashFrame >= DASH_FRAMES) {
            anim.dashing = false;
        } else {
            sprite.texture = _kt[`dash_${anim.dashDir}_${anim.dashFrame}`];
            return;
        }
    }

    if (anim.sk3ing) {
        anim.sk3Timer += TICK_DT;
        if (anim.sk3Timer >= SK3_TICK) {
            anim.sk3Timer -= SK3_TICK;
            anim.sk3Frame++;
        }
        if (anim.sk3Frame >= SK3_FRAMES) {
            anim.sk3ing = false;
        } else {
            sprite.texture = _kt[`sk3_south_${anim.sk3Frame}`];
            return;
        }
    }

    if (anim.sk2ing) {
        anim.sk2Timer += TICK_DT;
        if (anim.sk2Timer >= SK2_TICK) {
            anim.sk2Timer -= SK2_TICK;
            anim.sk2Frame++;
        }
        if (anim.sk2Frame >= SK2_FRAMES) {
            anim.sk2ing = false;
        } else {
            sprite.texture = _kt[`sk2_south_${anim.sk2Frame}`];
            return;
        }
    }

    if (anim.attacking) {
        anim.atkTimer += TICK_DT;
        if (anim.atkTimer >= ATK_TICK) {
            anim.atkTimer -= ATK_TICK;
            anim.atkFrame++;
        }
        if (anim.atkFrame >= ATK_FRAMES) {
            anim.attacking = false;
        } else {
            sprite.texture = _kt[`atk_${anim.facing}_${anim.atkFrame}`];
            return;
        }
    }

    if (moving) {
        anim.timer += TICK_DT;
        if (anim.timer >= RUN_TICK) {
            anim.timer -= RUN_TICK;
            anim.frame = (anim.frame + 1) % 4;
        }
        sprite.texture = _kt[`run_${anim.facing}_${anim.frame}`];
    } else {
        anim.frame = 0;
        anim.timer = 0;
        sprite.texture = _kt[anim.facing];
    }
}


// Logique de sprite Seris

function _updateSerisSprite(sprite, anim, moving) {
    // Priorité : téléport > vide (sk2) > éventail (sk1) > attaque > run > idle

    if (anim.steling) {
        anim.steleTimer += TICK_DT;
        if (anim.steleTimer >= S_TELE_TICK) {
            anim.steleTimer -= S_TELE_TICK;
            anim.steleFrame++;
        }
        if (anim.steleFrame >= S_TELE_FRAMES) {
            anim.steling = false;
        } else {
            sprite.texture = _st[`stele_south_${anim.steleFrame}`];
            return;
        }
    }

    if (anim.ssk2ing) {
        anim.ssk2Timer += TICK_DT;
        if (anim.ssk2Timer >= S_SK2_TICK) {
            anim.ssk2Timer -= S_SK2_TICK;
            anim.ssk2Frame++;
        }
        if (anim.ssk2Frame >= S_SK2_FRAMES) {
            anim.ssk2ing = false;
        } else {
            sprite.texture = _st[`ssk2_south_${anim.ssk2Frame}`];
            return;
        }
    }

    if (anim.ssk1ing) {
        anim.ssk1Timer += TICK_DT;
        if (anim.ssk1Timer >= S_SK1_TICK) {
            anim.ssk1Timer -= S_SK1_TICK;
            anim.ssk1Frame++;
        }
        if (anim.ssk1Frame >= S_SK1_FRAMES) {
            anim.ssk1ing = false;
        } else {
            sprite.texture = _st[`ssk1_south_${anim.ssk1Frame}`];
            return;
        }
    }

    if (anim.attacking) {
        anim.atkTimer += TICK_DT;
        if (anim.atkTimer >= S_ATK_TICK) {
            anim.atkTimer -= S_ATK_TICK;
            anim.atkFrame++;
        }
        if (anim.atkFrame >= S_ATK_FRAMES) {
            anim.attacking = false;
        } else {
            sprite.texture = _st[`satk_${anim.facing}_${anim.atkFrame}`];
            return;
        }
    }

    if (moving) {
        anim.timer += TICK_DT;
        if (anim.timer >= S_RUN_TICK) {
            anim.timer -= S_RUN_TICK;
            anim.frame = (anim.frame + 1) % S_RUN_FRAMES;
        }
        sprite.texture = _st[`srun_${anim.facing}_${anim.frame}`];
    } else {
        anim.frame = 0;
        anim.timer = 0;
        sprite.texture = _st[`s_${anim.facing}`];
    }
}


// Logique de sprite Aldric

function _updateAldricSprite(sprite, anim, moving) {
    // Priorité : envol > vague (sk2) > halo (sk1) > attaque > marche > idle

    if (anim.aflying) {
        anim.aflyTimer += TICK_DT;
        if (anim.aflyTimer >= A_FLY_TICK) {
            anim.aflyTimer -= A_FLY_TICK;
            anim.aflyFrame++;
        }
        if (anim.aflyFrame >= A_FLY_FRAMES) {
            anim.aflying = false;
        } else {
            sprite.texture = _at[`afly_south_${anim.aflyFrame}`];
            return;
        }
    }

    if (anim.ask2ing) {
        anim.ask2Timer += TICK_DT;
        if (anim.ask2Timer >= A_SK2_TICK) {
            anim.ask2Timer -= A_SK2_TICK;
            anim.ask2Frame++;
        }
        if (anim.ask2Frame >= A_SK2_FRAMES) {
            anim.ask2ing = false;
        } else {
            sprite.texture = _at[`ask2_south_${anim.ask2Frame}`];
            return;
        }
    }

    if (anim.ask1ing) {
        anim.ask1Timer += TICK_DT;
        if (anim.ask1Timer >= A_SK1_TICK) {
            anim.ask1Timer -= A_SK1_TICK;
            anim.ask1Frame++;
        }
        if (anim.ask1Frame >= A_SK1_FRAMES) {
            anim.ask1ing = false;
        } else {
            sprite.texture = _at[`ask1_south_${anim.ask1Frame}`];
            return;
        }
    }

    if (anim.attacking) {
        anim.atkTimer += TICK_DT;
        if (anim.atkTimer >= A_ATK_TICK) {
            anim.atkTimer -= A_ATK_TICK;
            anim.atkFrame++;
        }
        if (anim.atkFrame >= A_ATK_FRAMES) {
            anim.attacking = false;
        } else {
            sprite.texture = _at[`aatk_${anim.facing}_${anim.atkFrame}`];
            return;
        }
    }

    if (moving) {
        anim.timer += TICK_DT;
        if (anim.timer >= A_WK_TICK) {
            anim.timer -= A_WK_TICK;
            anim.frame = (anim.frame + 1) % A_WK_FRAMES;
        }
        sprite.texture = _at[`awk_${anim.facing}_${anim.frame}`];
    } else {
        anim.frame = 0;
        anim.timer = 0;
        sprite.texture = _at[`a_${anim.facing}`];
    }
}


// Helpers

// Renvoie la direction dominante ('south'|'north'|'east'|'west') ou null si stationnaire
function _detectDir(dx, dy) {
    if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01) return null;
    if (Math.abs(dx) >= Math.abs(dy)) return dx > 0 ? 'east' : 'west';
    return dy > 0 ? 'south' : 'north';
}


// Création du sprite

function _createPlayerGraphic(player) {
    const th        = getTh();
    const container = new Container();
    const r         = Math.max(10, Math.min(40, th * 0.4));

    const hboxG = new Graphics();
    container._hboxG = hboxG;

    if (player.class === 'kael' && _kaelReady) {
        const shadow = new Graphics();
        shadow.ellipse(0, r * 0.3, r * 0.5, r * 0.13).fill({ color: 0x000000, alpha: 0.3 });

        const scale  = (th * 1.2) / 48;
        const sprite = new Sprite(_kt.south);
        sprite.anchor.set(0.5, 0.75);
        sprite.scale.set(scale);
        container._sprite = sprite;

        const rangeG = new Graphics();
        container._rangeGraphic = rangeG;

        container.addChild(rangeG, shadow, sprite, hboxG);

    } else if (player.class === 'seris' && _serisReady) {
        const shadow = new Graphics();
        shadow.ellipse(0, r * 0.3, r * 0.5, r * 0.13).fill({ color: 0x000000, alpha: 0.3 });

        const scale  = (th * 1.2) / 48;
        const sprite = new Sprite(_st.s_south);
        sprite.anchor.set(0.5, 0.75);
        sprite.scale.set(scale);
        container._sprite = sprite;

        container.addChild(shadow, sprite, hboxG);

    } else if (player.class === 'aldric' && _aldricReady) {
        const shadow = new Graphics();
        shadow.ellipse(0, r * 0.3, r * 0.5, r * 0.13).fill({ color: 0x000000, alpha: 0.3 });

        const scale  = (th * 1.2) / 48;
        const sprite = new Sprite(_at.a_south);
        sprite.anchor.set(0.5, 0.75);
        sprite.scale.set(scale);
        container._sprite = sprite;

        container.addChild(shadow, sprite, hboxG);

    } else {
        const color = CLASS_COLORS[player.class] ?? 0xffffff;
        const body  = new Graphics();
        body.ellipse(0, r * 0.5, r * 0.6, r * 0.15).fill({ color: 0x000000, alpha: 0.25 });
        body.circle(0, 0, r).fill({ color });
        body.circle(0, 0, r).stroke({ color: 0xffffff, width: 1.5, alpha: 0.3 });

        const label = new Text({
            text: player.username,
            style: {
                fontSize:   Math.max(9, Math.min(14, th * 0.15)),
                fill:       0xdddddd,
                fontFamily: 'Courier New',
            },
        });
        label.anchor.set(0.5, 1);
        label.y = -(r * 1.05 + 8);

        container.addChild(body, label, hboxG);
    }

    return container;
}
