617 lines
24 KiB
JavaScript
617 lines
24 KiB
JavaScript
// renderEnemies.js : rendu des ennemis (sprites + barre de vie + ombre)
|
||
// pool de sprites reutilises (pas de recreate par frame)
|
||
|
||
import { Container, Graphics, Assets, Sprite } from 'pixi.js';
|
||
import { iso, getTh, getTw } from './renderer.js';
|
||
import { ENEMY_HITBOX_RADIUS } from './constants.js';
|
||
|
||
let _debugHitboxes = false;
|
||
export function setDebugHitboxes(v) { _debugHitboxes = v; }
|
||
|
||
// Assets Fracture
|
||
const _ft = {};
|
||
let _fractureReady = false;
|
||
|
||
export async function loadFractureAssets() {
|
||
if (_fractureReady) return;
|
||
const b = '../assets/sprites/fracture/';
|
||
|
||
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 entries = [
|
||
['f_south', b + 'rotations/south.png'],
|
||
['f_north', b + 'rotations/north.png'],
|
||
['f_east', b + 'rotations/east.png'],
|
||
['f_west', b + 'rotations/west.png'],
|
||
...anim('frun', D4, 4, 'running'),
|
||
];
|
||
|
||
for (const [key, path] of entries) {
|
||
const tex = await Assets.load(path);
|
||
tex.source.scaleMode = 'nearest';
|
||
_ft[key] = tex;
|
||
}
|
||
_fractureReady = true;
|
||
}
|
||
|
||
// Assets Rampant
|
||
const _rt = {};
|
||
let _rampantReady = false;
|
||
|
||
export async function loadRampantAssets() {
|
||
if (_rampantReady) return;
|
||
const b = '../assets/sprites/rampant/';
|
||
|
||
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 entries = [
|
||
['r_south', b + 'rotations/south.png'],
|
||
['r_north', b + 'rotations/north.png'],
|
||
['r_east', b + 'rotations/east.png'],
|
||
['r_west', b + 'rotations/west.png'],
|
||
...anim('rrun', D4, 6, 'running'),
|
||
];
|
||
|
||
for (const [key, path] of entries) {
|
||
const tex = await Assets.load(path);
|
||
tex.source.scaleMode = 'nearest';
|
||
_rt[key] = tex;
|
||
}
|
||
_rampantReady = true;
|
||
}
|
||
|
||
// Assets Colosse
|
||
const _ct = {};
|
||
let _colosseReady = false;
|
||
|
||
export async function loadColosseAssets() {
|
||
if (_colosseReady) return;
|
||
const b = '../assets/sprites/colosse/';
|
||
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 entries = [
|
||
['c_south', b + 'rotations/south.png'],
|
||
['c_north', b + 'rotations/north.png'],
|
||
['c_east', b + 'rotations/east.png'],
|
||
['c_west', b + 'rotations/west.png'],
|
||
...anim('crun', D4, 6, 'running'),
|
||
];
|
||
for (const [key, path] of entries) {
|
||
const tex = await Assets.load(path);
|
||
tex.source.scaleMode = 'nearest';
|
||
_ct[key] = tex;
|
||
}
|
||
_colosseReady = true;
|
||
}
|
||
|
||
// Assets Éclat
|
||
const _et = {};
|
||
let _eclatReady = false;
|
||
|
||
export async function loadEclatAssets() {
|
||
if (_eclatReady) return;
|
||
const b = '../assets/sprites/eclat/';
|
||
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 entries = [
|
||
['e_south', b + 'rotations/south.png'],
|
||
['e_north', b + 'rotations/north.png'],
|
||
['e_east', b + 'rotations/east.png'],
|
||
['e_west', b + 'rotations/west.png'],
|
||
...anim('erun', D4, 8, 'running'),
|
||
];
|
||
for (const [key, path] of entries) {
|
||
const tex = await Assets.load(path);
|
||
tex.source.scaleMode = 'nearest';
|
||
_et[key] = tex;
|
||
}
|
||
_eclatReady = true;
|
||
}
|
||
|
||
// Assets Vexaris
|
||
const _vt = {};
|
||
let _vexarisReady = false;
|
||
|
||
export async function loadVexarisAssets() {
|
||
if (_vexarisReady) return;
|
||
const b = '../assets/sprites/vexaris/';
|
||
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 entries = [
|
||
['v_south', b + 'rotations/south.png'],
|
||
['v_north', b + 'rotations/north.png'],
|
||
['v_east', b + 'rotations/east.png'],
|
||
['v_west', b + 'rotations/west.png'],
|
||
...anim('vrun', D4, 4, 'running'),
|
||
...anim('vatk', D4, 7, 'attack'),
|
||
...anim('vchg', D4, 7, 'charge'),
|
||
...anim('vbst', ['south'], 9, 'burst'),
|
||
];
|
||
for (const [key, path] of entries) {
|
||
const tex = await Assets.load(path);
|
||
tex.source.scaleMode = 'nearest';
|
||
_vt[key] = tex;
|
||
}
|
||
_vexarisReady = true;
|
||
}
|
||
|
||
// Animation state par ennemi (position précédente + état animation)
|
||
const _ePrevPos = {}; // id → { x, y }
|
||
const _eAnimState = {}; // id → { facing, frame, timer, action }
|
||
|
||
const F_RUN_FPS = 8; const F_RUN_TICK = 1 / F_RUN_FPS;
|
||
const R_RUN_FPS = 12; const R_RUN_TICK = 1 / R_RUN_FPS;
|
||
const C_RUN_FPS = 5; const C_RUN_TICK = 1 / C_RUN_FPS; // colosse très lent
|
||
const E_RUN_FPS = 16; const E_RUN_TICK = 1 / E_RUN_FPS; // éclat ultrarapide
|
||
const V_RUN_FPS = 8; const V_RUN_TICK = 1 / V_RUN_FPS; // running 4 frames
|
||
const V_ATK_FPS = 10; const V_ATK_TICK = 1 / V_ATK_FPS; // attack 7 frames
|
||
const V_CHG_FPS = 14; const V_CHG_TICK = 1 / V_CHG_FPS; // charge 7 frames
|
||
const V_BST_FPS = 8; const V_BST_TICK = 1 / V_BST_FPS; // burst 9 frames south
|
||
const TICK_DT = 0.05;
|
||
// iso() = projection monde → pixels isométriques
|
||
// getTh() = pixels par unité monde (pour les tailles proportionnelles)
|
||
|
||
|
||
// Couleur de chaque type d'ennemi (valeur hexadécimale RGB)
|
||
const ENEMY_COLORS = {
|
||
fracture: 0xcc3333, // rouge foncé — ennemi de base qui fonce sur le Soulgate
|
||
rampant: 0xff6600, // orange — attaque les joueurs
|
||
colosse: 0x882200, // brun-rouge — lent et résistant
|
||
eclat: 0xffcc00, // jaune — petit et rapide
|
||
vexaris: 0xaa22ff, // violet — boss vague 2
|
||
general: 0xcc6600, // orange foncé — sous-boss invoqué par Morveth
|
||
morveth: 0x550088, // violet très foncé — boss final vague 3
|
||
};
|
||
|
||
// Tailles des ennemis selon le type
|
||
// min/max = limites en pixels (pour les petits et grands écrans)
|
||
// scale = facteur multiplicateur de th (pixels par unité monde)
|
||
const ENEMY_SIZES = {
|
||
morveth: { min: 14, max: 55, scale: 0.65 }, // boss final = très grand
|
||
vexaris: { min: 12, max: 45, scale: 0.50 }, // boss vague 2 = grand
|
||
general: { min: 8, max: 30, scale: 0.32 }, // sous-boss = moyen
|
||
};
|
||
const DEFAULT_SIZE = { min: 6, max: 25, scale: 0.22 };
|
||
// Taille par défaut pour les ennemis normaux (fracture, rampant, colosse, eclat)
|
||
|
||
|
||
export function updateEnemies(layer, pool, enemies) {
|
||
// Met à jour les sprites de tous les ennemis depuis les données serveur
|
||
// layer = Container PixiJS du layer "ennemis"
|
||
// pool = { id → Container } — sprites existants
|
||
// enemies = tableau d'EnemyState reçus dans msg.enemies
|
||
|
||
// 1. Supprimer les sprites des ennemis morts (plus dans la liste serveur)
|
||
const ids = new Set(enemies.map(e => e.id));
|
||
// ids = Set des IDs encore vivants ce tick
|
||
|
||
for (const [id, container] of Object.entries(pool)) {
|
||
if (!ids.has(id)) {
|
||
layer.removeChild(container);
|
||
delete pool[id];
|
||
delete _ePrevPos[id];
|
||
delete _eAnimState[id];
|
||
}
|
||
}
|
||
|
||
// 2. Créer ou mettre à jour chaque ennemi
|
||
for (const e of enemies) {
|
||
// e = objet ennemi : { id, type, x, y, hp, max_hp, is_boss, frozen }
|
||
|
||
if (!pool[e.id]) {
|
||
pool[e.id] = _createEnemyGraphic(e.type);
|
||
layer.addChild(pool[e.id]);
|
||
_ePrevPos[e.id] = { x: e.x, y: e.y };
|
||
_eAnimState[e.id] = { facing: 'south', frame: 0, timer: 0 };
|
||
}
|
||
|
||
const ctr = pool[e.id];
|
||
const pos = iso(e.x, e.y);
|
||
const prev = _ePrevPos[e.id];
|
||
const anim = _eAnimState[e.id];
|
||
|
||
// Détecter direction depuis le déplacement
|
||
const ddx = e.x - prev.x;
|
||
const ddy = e.y - prev.y;
|
||
const moving = Math.abs(ddx) > 0.01 || Math.abs(ddy) > 0.01;
|
||
if (moving) anim.facing = _enemyDir(ddx, ddy) ?? anim.facing;
|
||
_ePrevPos[e.id] = { x: e.x, y: e.y };
|
||
|
||
ctr.x = pos.x;
|
||
ctr.y = pos.y;
|
||
|
||
// Sprite Fracture
|
||
if (e.type === 'fracture' && _fractureReady && ctr._sprite) {
|
||
anim.timer += TICK_DT;
|
||
if (anim.timer >= F_RUN_TICK) {
|
||
anim.timer -= F_RUN_TICK;
|
||
anim.frame = (anim.frame + 1) % 4;
|
||
}
|
||
ctr._sprite.texture = moving
|
||
? _ft[`frun_${anim.facing}_${anim.frame}`]
|
||
: _ft[`f_${anim.facing}`];
|
||
}
|
||
|
||
// Sprite Rampant
|
||
if (e.type === 'rampant' && _rampantReady && ctr._sprite) {
|
||
anim.timer += TICK_DT;
|
||
if (anim.timer >= R_RUN_TICK) {
|
||
anim.timer -= R_RUN_TICK;
|
||
anim.frame = (anim.frame + 1) % 6;
|
||
}
|
||
ctr._sprite.texture = moving
|
||
? _rt[`rrun_${anim.facing}_${anim.frame}`]
|
||
: _rt[`r_${anim.facing}`];
|
||
}
|
||
|
||
// Sprite Colosse
|
||
if (e.type === 'colosse' && _colosseReady && ctr._sprite) {
|
||
anim.timer += TICK_DT;
|
||
if (anim.timer >= C_RUN_TICK) {
|
||
anim.timer -= C_RUN_TICK;
|
||
anim.frame = (anim.frame + 1) % 6;
|
||
}
|
||
ctr._sprite.texture = moving
|
||
? _ct[`crun_${anim.facing}_${anim.frame}`]
|
||
: _ct[`c_${anim.facing}`];
|
||
}
|
||
|
||
// Sprite Éclat
|
||
if (e.type === 'eclat' && _eclatReady && ctr._sprite) {
|
||
anim.timer += TICK_DT;
|
||
if (anim.timer >= E_RUN_TICK) {
|
||
anim.timer -= E_RUN_TICK;
|
||
anim.frame = (anim.frame + 1) % 8;
|
||
}
|
||
ctr._sprite.texture = moving
|
||
? _et[`erun_${anim.facing}_${anim.frame}`]
|
||
: _et[`e_${anim.facing}`];
|
||
}
|
||
|
||
// Sprite Vexaris
|
||
if (e.type === 'vexaris' && _vexarisReady && ctr._sprite) {
|
||
const prevAction = anim.action ?? 'idle';
|
||
let action;
|
||
if (e.is_charging) {
|
||
action = 'charge';
|
||
} else if (moving) {
|
||
action = 'run';
|
||
} else if (anim.action === 'burst' && anim.frame < 8) {
|
||
action = 'burst'; // laisser le burst se terminer
|
||
} else {
|
||
action = 'attack';
|
||
}
|
||
if (action !== prevAction) { anim.frame = 0; anim.timer = 0; }
|
||
anim.action = action;
|
||
anim.timer += TICK_DT;
|
||
|
||
if (action === 'charge') {
|
||
if (anim.timer >= V_CHG_TICK) { anim.timer -= V_CHG_TICK; anim.frame = (anim.frame + 1) % 7; }
|
||
ctr._sprite.texture = _vt[`vchg_${anim.facing}_${anim.frame}`];
|
||
} else if (action === 'run') {
|
||
if (anim.timer >= V_RUN_TICK) { anim.timer -= V_RUN_TICK; anim.frame = (anim.frame + 1) % 4; }
|
||
ctr._sprite.texture = _vt[`vrun_${anim.facing}_${anim.frame}`];
|
||
} else if (action === 'burst') {
|
||
if (anim.timer >= V_BST_TICK) { anim.timer -= V_BST_TICK; anim.frame = Math.min(8, anim.frame + 1); }
|
||
ctr._sprite.texture = _vt[`vbst_south_${anim.frame}`];
|
||
} else {
|
||
if (anim.timer >= V_ATK_TICK) { anim.timer -= V_ATK_TICK; anim.frame = (anim.frame + 1) % 7; }
|
||
ctr._sprite.texture = _vt[`vatk_${anim.facing}_${anim.frame}`];
|
||
}
|
||
}
|
||
|
||
ctr.tint = e.frozen ? 0x88bbff : 0xffffff;
|
||
// Si gelé (sort divin Seris) → teinte bleue
|
||
// Sinon → blanc = pas de teinte (couleur de base)
|
||
// e.frozen = bool envoyé par le serveur (frozen_timer > 0)
|
||
|
||
_updateHealthBar(ctr, e.hp, e.max_hp);
|
||
|
||
// Debug : hitbox de collision
|
||
if (ctr._hboxG) {
|
||
const hg = ctr._hboxG;
|
||
hg.clear();
|
||
if (_debugHitboxes) {
|
||
const rx = ENEMY_HITBOX_RADIUS * getTw() / 2;
|
||
const ry = ENEMY_HITBOX_RADIUS * getTh() / 2;
|
||
hg.ellipse(0, 0, rx, ry).stroke({ color: 0xff4400, width: 1.5, alpha: 0.9 });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
function _updateHealthBar(ctr, hp, maxHp) {
|
||
// Met à jour la barre de vie d'un ennemi
|
||
// ctr = le Container de l'ennemi (contient barFg en tant que propriété custom)
|
||
// hp = points de vie actuels
|
||
// maxHp = points de vie max
|
||
|
||
const ratio = maxHp > 0 ? Math.max(0, hp / maxHp) : 0;
|
||
// ratio = pourcentage de vie restante (0.0 à 1.0)
|
||
// Math.max(0, ...) = éviter les valeurs négatives si hp < 0 (sécurité)
|
||
// maxHp > 0 ? ... : 0 = éviter la division par zéro
|
||
|
||
const barFg = ctr._barFg;
|
||
// _barFg = la Graphics de la barre de vie (stockée comme propriété custom sur le Container)
|
||
// Le _ devant = convention "propriété interne"
|
||
|
||
if (!barFg) return; // sécurité si le Container n'a pas été initialisé correctement
|
||
|
||
barFg.clear(); // effacer le dessin précédent de la barre
|
||
|
||
if (ratio > 0) {
|
||
// Redessiner la barre avec la nouvelle largeur proportionnelle au ratio
|
||
barFg.rect(ctr._barX, ctr._barY, ctr._barW * ratio, ctr._barH)
|
||
.fill({ color: 0xff3333 });
|
||
// ctr._barW * ratio = largeur totale × ratio HP → barre qui rétrécit
|
||
// 0xff3333 = rouge
|
||
}
|
||
// Si ratio = 0 → on ne dessine rien (ennemi presque mort)
|
||
}
|
||
|
||
|
||
function _enemyDir(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';
|
||
}
|
||
|
||
|
||
function _createEnemyGraphic(type) {
|
||
// Crée le Container PixiJS pour un ennemi (appelé une seule fois par ennemi)
|
||
// type = string : "fracture", "rampant", "colosse", "eclat", "vexaris", "morveth", "general"
|
||
|
||
const th = getTh(); // pixels par unité monde (pour les tailles proportionnelles)
|
||
const container = new Container();
|
||
|
||
// Calculer le rayon selon le type
|
||
const sz = ENEMY_SIZES[type] ?? DEFAULT_SIZE;
|
||
// ENEMY_SIZES[type] = taille spécifique si boss/général, sinon DEFAULT_SIZE
|
||
// ?? = operateur nullish coalescing : si null ou undefined → utiliser DEFAULT_SIZE
|
||
|
||
const r = Math.max(sz.min, Math.min(sz.max, th * sz.scale));
|
||
// th * sz.scale = taille proportionnelle au zoom
|
||
// Math.max(sz.min, Math.min(sz.max, ...)) = clamp entre min et max
|
||
|
||
const color = ENEMY_COLORS[type] ?? 0xff0000;
|
||
|
||
// Sprite Rampant
|
||
// Canvas 48×48, char bbox y=[8..41], anchor y=0.75 → anchor canvas=36
|
||
// Char top au-dessus de l'anchor = 36-8 = 28 canvas pixels
|
||
// Scale = (th*1.4)/48 → char top à screen y = -28*(th*1.4/48) = -th*0.817
|
||
if (type === 'rampant' && _rampantReady) {
|
||
const shadow = new Graphics();
|
||
shadow.ellipse(0, r * 0.4, r * 0.5, r * 0.12).fill({ color: 0x000000, alpha: 0.3 });
|
||
const scale = (th * 1.4) / 48;
|
||
const sprite = new Sprite(_rt.r_south);
|
||
sprite.anchor.set(0.5, 0.75);
|
||
sprite.scale.set(scale);
|
||
|
||
const barW = th * 0.7;
|
||
const barH = Math.max(2, th * 0.035);
|
||
const barY = -(th * 0.82 + barH + 8); // 8px marge au-dessus de la tête
|
||
const barBg = new Graphics();
|
||
barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
|
||
const barFg = new Graphics();
|
||
barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
|
||
const hboxG = new Graphics();
|
||
container.addChild(shadow, sprite, barBg, barFg, hboxG);
|
||
container._sprite = sprite;
|
||
container._barFg = barFg;
|
||
container._barX = -barW / 2;
|
||
container._barY = barY;
|
||
container._barW = barW;
|
||
container._barH = barH;
|
||
container._hboxG = hboxG;
|
||
return container;
|
||
}
|
||
|
||
// Sprite Fracture
|
||
// Canvas 68×68, char bbox y=[10..58], anchor y=0.75 → anchor canvas=51
|
||
// Char top au-dessus de l'anchor = 51-10 = 41 canvas pixels
|
||
// Scale = (th*1.2)/48 → char top à screen y = -41*(th*1.2/48) = -th*1.025
|
||
if (type === 'fracture' && _fractureReady) {
|
||
const shadow = new Graphics();
|
||
shadow.ellipse(0, r * 0.5, r * 0.6, r * 0.15).fill({ color: 0x000000, alpha: 0.3 });
|
||
const scale = (th * 1.2) / 48;
|
||
const sprite = new Sprite(_ft.f_south);
|
||
sprite.anchor.set(0.5, 0.75);
|
||
sprite.scale.set(scale);
|
||
|
||
const barW = th * 0.8;
|
||
const barH = Math.max(2, th * 0.04);
|
||
const barY = -(th * 1.025 + barH + 10); // 10px marge au-dessus de la tête
|
||
const barBg = new Graphics();
|
||
barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
|
||
const barFg = new Graphics();
|
||
barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
|
||
const hboxG = new Graphics();
|
||
container.addChild(shadow, sprite, barBg, barFg, hboxG);
|
||
container._sprite = sprite;
|
||
container._barFg = barFg;
|
||
container._barX = -barW / 2;
|
||
container._barY = barY;
|
||
container._barW = barW;
|
||
container._barH = barH;
|
||
container._hboxG = hboxG;
|
||
return container;
|
||
}
|
||
|
||
// Sprite Colosse
|
||
// Canvas 92×92, char top à y=13, anchor y=0.75 → anchor=69, dist=56px
|
||
// Scale = (th*2.0)/92 → char_top = -56*(th*2.0/92) = -th*1.217
|
||
if (type === 'colosse' && _colosseReady) {
|
||
const shadow = new Graphics();
|
||
shadow.ellipse(0, r * 0.6, r * 0.9, r * 0.22).fill({ color: 0x000000, alpha: 0.35 });
|
||
const scale = (th * 2.0) / 92;
|
||
const sprite = new Sprite(_ct.c_south);
|
||
sprite.anchor.set(0.5, 0.75);
|
||
sprite.scale.set(scale);
|
||
const barW = th * 1.2;
|
||
const barH = Math.max(2, th * 0.05);
|
||
const barY = -(th * 1.22 + barH + 10);
|
||
const barBg = new Graphics();
|
||
barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
|
||
const barFg = new Graphics();
|
||
barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
|
||
const hboxG = new Graphics();
|
||
container.addChild(shadow, sprite, barBg, barFg, hboxG);
|
||
container._sprite = sprite;
|
||
container._barFg = barFg;
|
||
container._barX = -barW / 2;
|
||
container._barY = barY;
|
||
container._barW = barW;
|
||
container._barH = barH;
|
||
container._hboxG = hboxG;
|
||
return container;
|
||
}
|
||
|
||
// Sprite Vexaris
|
||
// Canvas 92×92, char top à y≈12, anchor y=0.75 → anchor=69, dist=57px
|
||
// Scale = (th*2.2)/92 → char_top = -57*(th*2.2/92) = -th*1.363
|
||
if (type === 'vexaris' && _vexarisReady) {
|
||
const shadow = new Graphics();
|
||
shadow.ellipse(0, r * 0.7, r * 1.1, r * 0.28).fill({ color: 0x330033, alpha: 0.45 });
|
||
const scale = (th * 2.2) / 92;
|
||
const sprite = new Sprite(_vt.v_south);
|
||
sprite.anchor.set(0.5, 0.75);
|
||
sprite.scale.set(scale);
|
||
const barW = th * 1.6;
|
||
const barH = Math.max(3, th * 0.06);
|
||
const barY = -(th * 1.36 + barH + 12);
|
||
const barBg = new Graphics();
|
||
barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220022 });
|
||
const barFg = new Graphics();
|
||
barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xaa22ff });
|
||
const hboxG = new Graphics();
|
||
container.addChild(shadow, sprite, barBg, barFg, hboxG);
|
||
container._sprite = sprite;
|
||
container._barFg = barFg;
|
||
container._barX = -barW / 2;
|
||
container._barY = barY;
|
||
container._barW = barW;
|
||
container._barH = barH;
|
||
container._hboxG = hboxG;
|
||
return container;
|
||
}
|
||
|
||
// Sprite Éclat
|
||
// Canvas 36×36, char top à y=4, anchor y=0.75 → anchor=27, dist=23px
|
||
// Scale = (th*1.0)/36 → char_top = -23*(th/36) = -th*0.639
|
||
if (type === 'eclat' && _eclatReady) {
|
||
const shadow = new Graphics();
|
||
shadow.ellipse(0, r * 0.3, r * 0.4, r * 0.10).fill({ color: 0x000000, alpha: 0.25 });
|
||
const scale = (th * 1.0) / 36;
|
||
const sprite = new Sprite(_et.e_south);
|
||
sprite.anchor.set(0.5, 0.75);
|
||
sprite.scale.set(scale);
|
||
const barW = th * 0.5;
|
||
const barH = Math.max(2, th * 0.03);
|
||
const barY = -(th * 0.64 + barH + 6);
|
||
const barBg = new Graphics();
|
||
barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
|
||
const barFg = new Graphics();
|
||
barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
|
||
const hboxG = new Graphics();
|
||
container.addChild(shadow, sprite, barBg, barFg, hboxG);
|
||
container._sprite = sprite;
|
||
container._barFg = barFg;
|
||
container._barX = -barW / 2;
|
||
container._barY = barY;
|
||
container._barW = barW;
|
||
container._barH = barH;
|
||
container._hboxG = hboxG;
|
||
return container;
|
||
}
|
||
|
||
const body = new Graphics();
|
||
|
||
// Ombre au sol
|
||
body.ellipse(0, r * 0.5, r * 0.7, r * 0.18).fill({ color: 0x000000, alpha: 0.25 });
|
||
// Ellipse aplatie légèrement en dessous du centre (r*0.5) → simuler une ombre
|
||
|
||
// Cercle principal de l'ennemi
|
||
body.circle(0, 0, r).fill({ color });
|
||
// Contour blanc subtil
|
||
body.circle(0, 0, r).stroke({ color: 0xffffff, width: 1, alpha: 0.2 });
|
||
|
||
// Barre de vie
|
||
const barW = r * 2.4; // largeur totale de la barre = légèrement plus large que le cercle
|
||
const barH = Math.max(2, th * 0.04); // hauteur de la barre (au moins 2px)
|
||
const barY = -(r * 1.1 + barH + 3); // position Y = au-dessus du cercle (négatif = vers le haut)
|
||
|
||
const barBg = new Graphics();
|
||
// Fond de la barre (rouge très foncé = barre vide)
|
||
barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
|
||
// rect(x, y, largeur, hauteur) — centré horizontalement (-barW/2)
|
||
|
||
const barFg = new Graphics();
|
||
// Remplissage de la barre (rouge vif = vie restante)
|
||
// Dessiné à 100% au départ, puis mis à jour par _updateHealthBar()
|
||
barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
|
||
|
||
const hboxG = new Graphics();
|
||
|
||
container.addChild(body, barBg, barFg, hboxG);
|
||
// Ordre d'ajout = ordre d'affichage (barFg par-dessus barBg par-dessus body)
|
||
|
||
// Stocker les infos de la barre sur le Container pour les réutiliser dans _updateHealthBar
|
||
// On les préfixe avec _ pour indiquer que c'est de l'état interne
|
||
container._barFg = barFg; // référence au Graphics à redessiner
|
||
container._barX = -barW / 2; // X de départ de la barre
|
||
container._barY = barY; // Y de la barre
|
||
container._barW = barW; // largeur TOTALE (× ratio HP = largeur réelle)
|
||
container._barH = barH; // hauteur de la barre
|
||
container._hboxG = hboxG; // Graphics de la hitbox debug
|
||
|
||
return container;
|
||
}
|