SOULGATE/client/js/renderPlayers.js
2026-05-04 03:42:47 +02:00

736 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}