Applique .gitattributes sur tous les fichiers existants. Élimine les différences fantômes entre WSL et Windows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
467 lines
21 KiB
TypeScript
Executable File
467 lines
21 KiB
TypeScript
Executable File
import type { CommandBus } from '@application/handlers/CommandBus';
|
|
import type { QueryBus } from '@application/handlers/QueryBus';
|
|
import type { Enclos } from '@domain/entities/Enclos';
|
|
import type { Dragodinde } from '@domain/entities/Dragodinde';
|
|
import type { GaugeType, StatType } from '@domain/value-objects/GaugeType';
|
|
import { GAUGE_DEFS, STAT_DEFS } from '@domain/value-objects/GaugeType';
|
|
import { tierNum, tierRate } from '@domain/value-objects/Tier';
|
|
import { computeGaugeLive, calcSerenEtaLive, calcLevelEtaLive, calcLevel200EtaLive, elapsedLive } from '@presentation/helpers/gauge-live';
|
|
import { esc, fmt, fmtClock } from '@presentation/helpers/format';
|
|
import { Toast } from './Toast';
|
|
import { ConfirmModal } from './ConfirmModal';
|
|
import { UndoManager } from '@presentation/services/UndoManager';
|
|
|
|
interface StatPillDef {
|
|
key: StatType;
|
|
icon: string;
|
|
color: string;
|
|
min: number;
|
|
max: number;
|
|
}
|
|
|
|
const STAT_PILLS: StatPillDef[] = [
|
|
{ key: 'serenite', icon: 'sentiment_satisfied_alt', color: '96,165,250', min: -5000, max: 5000 },
|
|
{ key: 'endurance', icon: 'bolt', color: '250,204,21', min: 0, max: 20000 },
|
|
{ key: 'maturite', icon: 'water_drop', color: '34,211,238', min: 0, max: 20000 },
|
|
{ key: 'amour', icon: 'favorite', color: '248,113,113', min: 0, max: 20000 },
|
|
{ key: 'xp', icon: 'star', color: '254,240,138', min: 1, max: 200 },
|
|
];
|
|
|
|
export class DragodindeCard {
|
|
private el: HTMLElement | null = null;
|
|
private enclosId = 0;
|
|
private ddId = 0;
|
|
private lastTick = -1;
|
|
|
|
constructor(
|
|
private commandBus: CommandBus,
|
|
private queryBus: QueryBus,
|
|
private onReorder?: () => void,
|
|
) {}
|
|
|
|
render(container: HTMLElement, enclosId: number, ddId: number): void {
|
|
this.enclosId = enclosId;
|
|
this.ddId = ddId;
|
|
|
|
this.el = document.createElement('div');
|
|
this.el.className = 'dd-card enc-dd-card';
|
|
this.el.id = `ddc-${enclosId}-${ddId}`;
|
|
this.el.draggable = true;
|
|
|
|
this.el.addEventListener('dragstart', (e) => {
|
|
e.dataTransfer!.effectAllowed = 'move';
|
|
e.dataTransfer?.setData('text/dd-id', String(ddId));
|
|
e.dataTransfer?.setData('text/enc-id', String(enclosId));
|
|
// Délai pour que le navigateur capture le snapshot avant d'appliquer l'opacité
|
|
requestAnimationFrame(() => this.el!.classList.add('dragging'));
|
|
});
|
|
this.el.addEventListener('dragend', () => {
|
|
this.el!.classList.remove('dragging');
|
|
this.el!.classList.remove('drag-over');
|
|
});
|
|
this.el.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.dataTransfer!.dropEffect = 'move';
|
|
this.el!.classList.add('drag-over');
|
|
});
|
|
this.el.addEventListener('dragleave', (e) => {
|
|
// Ignorer si on entre dans un élément enfant
|
|
if (this.el!.contains(e.relatedTarget as Node)) return;
|
|
this.el!.classList.remove('drag-over');
|
|
});
|
|
this.el.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
this.el!.classList.remove('drag-over');
|
|
const srcDdId = e.dataTransfer?.getData('text/dd-id');
|
|
const srcEncId = e.dataTransfer?.getData('text/enc-id');
|
|
if (srcEncId === String(enclosId) && srcDdId && srcDdId !== String(ddId)) {
|
|
this.commandBus.execute({
|
|
type: 'reorder-dragodinde',
|
|
enclosId,
|
|
fromDdId: Number(srcDdId),
|
|
toDdId: ddId,
|
|
});
|
|
this.onReorder?.();
|
|
}
|
|
});
|
|
|
|
container.appendChild(this.el);
|
|
this.renderInner();
|
|
}
|
|
|
|
private renderInner(): void {
|
|
if (!this.el) return;
|
|
const enc = this.queryBus.execute<Enclos>({ type: 'get-enclos-detail', enclosId: this.enclosId });
|
|
const dd = enc.dragodindes.find(d => d.id === this.ddId);
|
|
if (!dd) return;
|
|
|
|
const eId = this.enclosId;
|
|
const dId = this.ddId;
|
|
|
|
/* stat pills */
|
|
const pillsHtml = STAT_PILLS.map(sp => {
|
|
const val = dd.stats[sp.key];
|
|
const atMax = val >= sp.max;
|
|
const atMin = sp.key === 'serenite' && val <= sp.min;
|
|
const atLimit = atMax || atMin;
|
|
return `<div class="dd-stat-pill enc-dd-stat-badge${atLimit ? ' at-max' : ''}" style="border-color:rgba(${sp.color},0.5)${atLimit ? `;box-shadow:0 0 8px rgba(${sp.color},0.5);background:rgba(${sp.color},0.18)` : ''}">
|
|
<span class="material-symbols-outlined enc-dd-stat-icon" style="color:rgb(${sp.color})">${sp.icon}</span>
|
|
<input type="number" class="pill-input enc-dd-stat-input"
|
|
id="pstat-${eId}-${dId}-${sp.key}"
|
|
data-stat="${sp.key}" data-prev="${val}"
|
|
min="${sp.min}" max="${sp.max}"
|
|
value="${val}">
|
|
<span class="pill-delta" id="pill-delta-${eId}-${dId}-${sp.key}"></span>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
/* Mapping icônes Material Symbols pour boutons jauges */
|
|
const GAUGE_MS_ICONS: Partial<Record<string, string>> = {
|
|
baffeur: 'remove', caresseur: 'add', foudroyeur: 'bolt',
|
|
abreuvoir: 'water_drop', dragofesse: 'favorite',
|
|
};
|
|
|
|
/* active gauge blocks */
|
|
const gaugeBlocksHtml = enc.activeGauges.map(gid => {
|
|
const def = GAUGE_DEFS[gid];
|
|
if (gid === 'mangeoire') {
|
|
return `<div class="enc-dd-gauge-block enc-dd-gauge-xp" data-gid="${gid}">
|
|
<div class="enc-dd-xp-main">
|
|
<div class="enc-dd-xp-left">
|
|
<span class="enc-dd-xp-niv" id="slv-${eId}-${dId}-${gid}" style="color:var(${def.cssVar})">NIV. 1</span>
|
|
<span class="enc-dd-xp-sub">XP <span id="eta200-pct-${eId}-${dId}">0%</span></span>
|
|
</div>
|
|
<span class="live-cd enc-dd-xp-cd" id="scd-${eId}-${dId}-${gid}">--:--:--</span>
|
|
</div>
|
|
<span id="eta200-${eId}-${dId}" class="enc-dd-xp-eta200">→ NIV. 200 : —</span>
|
|
<div class="enc-dd-bar-bg">
|
|
<div class="enc-dd-bar-fill" id="eta200-bar-${eId}-${dId}" style="width:0%;background:var(${def.cssVar})"></div>
|
|
</div>
|
|
<div class="enc-dd-bar-bg" style="margin-top:4px">
|
|
<div class="enc-dd-bar-fill" id="spb-${eId}-${dId}-${gid}" style="width:0%;background:var(${def.cssVar})"></div>
|
|
</div>
|
|
<span class="live-delta" id="sdelta-${eId}-${dId}-${gid}" style="display:none"></span>
|
|
</div>`;
|
|
}
|
|
const msIcon = GAUGE_MS_ICONS[gid] ?? 'circle';
|
|
return `<div class="enc-dd-gauge-block" data-gid="${gid}">
|
|
<div class="enc-dd-gauge-btn">
|
|
<span class="enc-dd-gauge-btn-left">
|
|
<span class="material-symbols-outlined" style="font-size:14px">${msIcon}</span>
|
|
<span class="enc-dd-gauge-btn-name">${def.label.toUpperCase()}</span>
|
|
</span>
|
|
<span class="live-val" id="slv-${eId}-${dId}-${gid}" style="display:none"></span>
|
|
<span class="live-delta" id="sdelta-${eId}-${dId}-${gid}" style="display:none"></span>
|
|
<span class="live-cd enc-dd-gauge-btn-cd" id="scd-${eId}-${dId}-${gid}">--:--:--</span>
|
|
</div>
|
|
<div class="enc-dd-bar-bg">
|
|
<div class="enc-dd-bar-fill" id="spb-${eId}-${dId}-${gid}" style="width:0%;background:var(${def.cssVar})"></div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
this.el.innerHTML = `
|
|
<div class="enc-dd-done-badge" id="dd-done-${eId}-${dId}" style="display:none">✓ TERMINÉ</div>
|
|
<div class="enc-dd-card-head">
|
|
<span class="dd-drag-handle enc-dd-drag-handle" title="Déplacer">⠿</span>
|
|
<input type="text" class="dd-name enc-dd-name-input" value="${esc(dd.name)}"
|
|
data-prev="${esc(dd.name)}" id="ddname-${eId}-${dId}">
|
|
<button class="dd-del enc-dd-del-btn" title="Supprimer">
|
|
<span class="material-symbols-outlined" style="font-size:16px">close</span>
|
|
</button>
|
|
</div>
|
|
<div class="enc-dd-body">
|
|
<div class="enc-dd-stats-grid">${pillsHtml}</div>
|
|
<div class="enc-dd-cibles">
|
|
<div class="enc-dd-cible-row">
|
|
<div class="enc-dd-cible-left">
|
|
<span class="material-symbols-outlined" style="font-size:18px;color:rgb(96,165,250)">sentiment_satisfied_alt</span>
|
|
<span class="enc-dd-cible-lbl">Cible</span>
|
|
</div>
|
|
<div class="enc-dd-cible-right">
|
|
<input type="number" class="enc-dd-cible-inp" id="ser-tgt-${eId}-${dId}"
|
|
min="${enc.activeGauges.includes('baffeur') ? '-5000' : '0'}"
|
|
max="${enc.activeGauges.includes('caresseur') ? '5000' : '0'}"
|
|
value="${dd.sereniteTarget ?? ''}" placeholder="${enc.activeGauges.includes('baffeur') ? '-5000…0' : enc.activeGauges.includes('caresseur') ? '0…5000' : '—'}">
|
|
<button class="enc-dd-cible-clr" id="ser-clr-${eId}-${dId}" title="Réinitialiser" ${dd.sereniteTarget == null ? 'style="visibility:hidden"' : ''}>✕</button>
|
|
<span class="enc-dd-cible-eta" id="ser-eta-${eId}-${dId}">—</span>
|
|
</div>
|
|
</div>
|
|
<div class="enc-dd-cible-row">
|
|
<div class="enc-dd-cible-left">
|
|
<span class="material-symbols-outlined" style="font-size:18px;color:rgb(234,179,8)">stars</span>
|
|
<span class="enc-dd-cible-lbl">Niveau</span>
|
|
</div>
|
|
<div class="enc-dd-cible-right">
|
|
<input type="number" class="enc-dd-cible-inp" id="lvl-tgt-${eId}-${dId}"
|
|
min="1" max="200"
|
|
value="${dd.levelTarget ?? ''}" placeholder="—">
|
|
<button class="enc-dd-cible-clr" id="lvl-clr-${eId}-${dId}" title="Réinitialiser" ${dd.levelTarget == null ? 'style="visibility:hidden"' : ''}>✕</button>
|
|
<span class="enc-dd-cible-eta" id="lvl-eta-${eId}-${dId}">—</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="enc-dd-gauge-blocks">${gaugeBlocksHtml}</div>
|
|
</div>
|
|
`;
|
|
|
|
this.bindEvents(dd);
|
|
}
|
|
|
|
private bindEvents(dd: Dragodinde): void {
|
|
if (!this.el) return;
|
|
const eId = this.enclosId;
|
|
const dId = this.ddId;
|
|
|
|
/* Delete button */
|
|
const delBtn = this.el.querySelector('.dd-del');
|
|
delBtn?.addEventListener('click', async () => {
|
|
const ok = await ConfirmModal.show('Retirer la dragodinde', 'Retirer cette dragodinde de l\'enclos ?');
|
|
if (!ok) return;
|
|
const hasSnap = await UndoManager.snapshotCurrent('Dragodinde retirée');
|
|
this.commandBus.execute({ type: 'remove-dragodinde', enclosId: eId, ddId: dId });
|
|
Toast.show('success', 'Dragodinde retirée.', hasSnap ? { label: 'Annuler', callback: () => UndoManager.undo() } : undefined);
|
|
});
|
|
|
|
/* Name input */
|
|
const nameInput = this.el.querySelector<HTMLInputElement>('.dd-name');
|
|
if (nameInput) {
|
|
nameInput.addEventListener('focus', () => {
|
|
nameInput.dataset.prev = nameInput.value;
|
|
nameInput.value = '';
|
|
});
|
|
nameInput.addEventListener('blur', () => {
|
|
const v = nameInput.value.trim();
|
|
if (!v) nameInput.value = nameInput.dataset.prev || dd.name;
|
|
else this.commandBus.execute({ type: 'rename-dragodinde', enclosId: eId, ddId: dId, name: v });
|
|
});
|
|
nameInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') { nameInput.value = nameInput.dataset.prev || dd.name; nameInput.blur(); }
|
|
else if (e.key === 'Enter') nameInput.blur();
|
|
});
|
|
}
|
|
|
|
/* Stat pill inputs */
|
|
this.el.querySelectorAll<HTMLInputElement>('.pill-input').forEach(inp => {
|
|
const stat = inp.dataset.stat as StatType;
|
|
inp.addEventListener('focus', () => {
|
|
inp.dataset.prev = inp.value;
|
|
inp.value = '';
|
|
});
|
|
inp.addEventListener('input', () => {
|
|
if (!inp.value) return;
|
|
const v = Number(inp.value);
|
|
if (!isNaN(v)) this.commandBus.execute({ type: 'update-dd-stat', enclosId: eId, ddId: dId, stat, value: v });
|
|
});
|
|
inp.addEventListener('blur', () => {
|
|
if (inp.value === '') { inp.value = inp.dataset.prev || '0'; return; }
|
|
const v = Number(inp.value);
|
|
if (!isNaN(v)) this.commandBus.execute({ type: 'update-dd-stat', enclosId: eId, ddId: dId, stat, value: v });
|
|
});
|
|
inp.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') { inp.value = inp.dataset.prev || '0'; inp.blur(); }
|
|
else if (e.key === 'Enter') inp.blur();
|
|
});
|
|
});
|
|
|
|
/* Clear buttons */
|
|
this.el.querySelector(`#ser-clr-${eId}-${dId}`)?.addEventListener('click', () => {
|
|
this.commandBus.execute({ type: 'update-dd-seren-target', enclosId: eId, ddId: dId, target: null });
|
|
const inp = this.el?.querySelector<HTMLInputElement>(`#ser-tgt-${eId}-${dId}`);
|
|
if (inp) inp.value = '';
|
|
const btn = this.el?.querySelector<HTMLElement>(`#ser-clr-${eId}-${dId}`);
|
|
if (btn) btn.style.visibility = 'hidden';
|
|
});
|
|
this.el.querySelector(`#lvl-clr-${eId}-${dId}`)?.addEventListener('click', () => {
|
|
this.commandBus.execute({ type: 'update-dd-level-target', enclosId: eId, ddId: dId, target: null });
|
|
const inp = this.el?.querySelector<HTMLInputElement>(`#lvl-tgt-${eId}-${dId}`);
|
|
if (inp) inp.value = '';
|
|
const btn = this.el?.querySelector<HTMLElement>(`#lvl-clr-${eId}-${dId}`);
|
|
if (btn) btn.style.visibility = 'hidden';
|
|
});
|
|
|
|
/* Serenite target — clamp selon la jauge active (baffeur → négatif, caresseur → positif) */
|
|
const serTgt = this.el.querySelector<HTMLInputElement>(`#ser-tgt-${eId}-${dId}`);
|
|
if (serTgt) {
|
|
const clampSeren = (raw: number): number => {
|
|
const enc = this.queryBus.execute<Enclos>({ type: 'get-enclos-detail', enclosId: eId });
|
|
if (enc.activeGauges.includes('baffeur')) return Math.min(0, Math.max(-5000, raw));
|
|
if (enc.activeGauges.includes('caresseur')) return Math.max(0, Math.min(5000, raw));
|
|
return Math.min(5000, Math.max(-5000, raw));
|
|
};
|
|
serTgt.addEventListener('focus', () => { serTgt.dataset.prev = serTgt.value; serTgt.value = ''; });
|
|
serTgt.addEventListener('input', () => {
|
|
if (!serTgt.value) return;
|
|
const v = Number(serTgt.value);
|
|
if (isNaN(v)) return;
|
|
const clamped = clampSeren(v);
|
|
this.commandBus.execute({ type: 'update-dd-seren-target', enclosId: eId, ddId: dId, target: clamped });
|
|
});
|
|
serTgt.addEventListener('blur', () => {
|
|
if (serTgt.value === '') { serTgt.value = serTgt.dataset.prev || ''; return; }
|
|
const v = Number(serTgt.value);
|
|
if (isNaN(v)) { serTgt.value = serTgt.dataset.prev || ''; return; }
|
|
const clamped = clampSeren(v);
|
|
this.commandBus.execute({ type: 'update-dd-seren-target', enclosId: eId, ddId: dId, target: clamped });
|
|
serTgt.value = String(clamped);
|
|
});
|
|
serTgt.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') { serTgt.value = serTgt.dataset.prev || ''; serTgt.blur(); }
|
|
else if (e.key === 'Enter') serTgt.blur();
|
|
});
|
|
}
|
|
|
|
/* Level target */
|
|
const lvlTgt = this.el.querySelector<HTMLInputElement>(`#lvl-tgt-${eId}-${dId}`);
|
|
if (lvlTgt) {
|
|
lvlTgt.addEventListener('focus', () => { lvlTgt.dataset.prev = lvlTgt.value; lvlTgt.value = ''; });
|
|
lvlTgt.addEventListener('input', () => {
|
|
if (!lvlTgt.value) return;
|
|
const v = Number(lvlTgt.value);
|
|
this.commandBus.execute({ type: 'update-dd-level-target', enclosId: eId, ddId: dId, target: isNaN(v) ? null : v });
|
|
});
|
|
lvlTgt.addEventListener('blur', () => {
|
|
if (lvlTgt.value === '') { lvlTgt.value = lvlTgt.dataset.prev || ''; return; }
|
|
const v = Number(lvlTgt.value);
|
|
this.commandBus.execute({ type: 'update-dd-level-target', enclosId: eId, ddId: dId, target: isNaN(v) ? null : v });
|
|
});
|
|
lvlTgt.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') { lvlTgt.value = lvlTgt.dataset.prev || ''; lvlTgt.blur(); }
|
|
else if (e.key === 'Enter') lvlTgt.blur();
|
|
});
|
|
}
|
|
}
|
|
|
|
update(enc: Enclos, dd: Dragodinde, el: number, started: boolean): void {
|
|
if (!this.el) return;
|
|
const eId = this.enclosId;
|
|
const dId = this.ddId;
|
|
|
|
/* Détection de tick (toutes les 10 sec).
|
|
* Après complétion automatique, le temps réel continue pour que les animations
|
|
* continuent sur toutes les jauges qui se vident en fond. */
|
|
const elForTick = started ? elapsedLive(enc) : 0;
|
|
const tick = started ? Math.floor(elForTick / 10) : -1;
|
|
if (!started) {
|
|
this.lastTick = -1;
|
|
} else if (this.lastTick === -1) {
|
|
this.lastTick = tick; // initialise sans déclencher au démarrage
|
|
}
|
|
const newTick = started && tick !== this.lastTick;
|
|
if (newTick) this.lastTick = tick;
|
|
|
|
let allDone = enc.activeGauges.length > 0;
|
|
|
|
/* Update active gauge blocks */
|
|
enc.activeGauges.forEach(gid => {
|
|
const r = computeGaugeLive(enc, dd, gid, el, started);
|
|
const def = GAUGE_DEFS[gid];
|
|
|
|
// Toutes les jauges comptent pour le badge "✓ TERMINÉ"
|
|
if (!r.done) allDone = false;
|
|
|
|
const lvEl = this.el!.querySelector(`#slv-${eId}-${dId}-${gid}`);
|
|
if (lvEl) lvEl.textContent = r.liveText;
|
|
|
|
/* Delta live-delta : pop à chaque tick.
|
|
* Continue jusqu'au cap absolu de la stat (pas juste la cible). */
|
|
const sd = def.isXp ? null : STAT_DEFS[def.stat as keyof typeof STAT_DEFS];
|
|
const atAbsCap = def.isXp
|
|
? (r.estStat as number) >= 200
|
|
: (def.dir > 0 ? (r.estStat as number) >= sd!.max : (r.estStat as number) <= sd!.min);
|
|
const deltaActive = started && !atAbsCap;
|
|
const deltaEl = this.el!.querySelector<HTMLElement>(`#sdelta-${eId}-${dId}-${gid}`);
|
|
if (deltaEl) {
|
|
deltaEl.textContent = r.deltaText;
|
|
if (newTick && deltaActive) {
|
|
deltaEl.classList.remove('show');
|
|
void deltaEl.offsetWidth; // force reflow pour relancer l'animation
|
|
deltaEl.classList.add('show');
|
|
} else if (!deltaActive) {
|
|
deltaEl.classList.remove('show');
|
|
}
|
|
}
|
|
|
|
const cdEl = this.el!.querySelector(`#scd-${eId}-${dId}-${gid}`);
|
|
if (cdEl) cdEl.textContent = r.done ? '✅' : (!isFinite(r.cntDown) ? '∞' : fmtClock(r.cntDown));
|
|
|
|
/* ETA + barre de progression niveau 200 (mangeoire uniquement) */
|
|
if (gid === 'mangeoire') {
|
|
const eta200El = this.el!.querySelector(`#eta200-${eId}-${dId}`);
|
|
if (eta200El) {
|
|
const eta = calcLevel200EtaLive(enc, dd, el, started);
|
|
eta200El.textContent = `→ NIV. 200 : ${eta || '—'}`;
|
|
}
|
|
const pct200 = Math.min(100, Math.max(0, ((r.estStat as number) - 1) / 199 * 100));
|
|
const barFillEl = this.el!.querySelector<HTMLElement>(`#eta200-bar-${eId}-${dId}`);
|
|
if (barFillEl) barFillEl.style.width = `${pct200.toFixed(1)}%`;
|
|
const pctEl = this.el!.querySelector<HTMLElement>(`#eta200-pct-${eId}-${dId}`);
|
|
if (pctEl) pctEl.textContent = `${Math.round(pct200)}%`;
|
|
}
|
|
|
|
const pbEl = this.el!.querySelector<HTMLElement>(`#spb-${eId}-${dId}-${gid}`);
|
|
if (pbEl) pbEl.style.width = `${r.progPct.toFixed(1)}%`;
|
|
|
|
/* Mise à jour live du badge de stat correspondant.
|
|
* Désactivée après complétion (__done__) : dd.stats est déjà à jour
|
|
* et l'utilisateur doit pouvoir corriger les valeurs à la main. */
|
|
if (started && !enc.alerted['__done__']) {
|
|
const pillInput = this.el!.querySelector<HTMLInputElement>(`#pstat-${eId}-${dId}-${def.stat}`);
|
|
if (pillInput && document.activeElement !== pillInput) {
|
|
pillInput.value = String(Math.round(r.estStat as number));
|
|
}
|
|
const sp = STAT_PILLS.find(p => p.key === def.stat);
|
|
if (sp) {
|
|
const val = Math.round(r.estStat as number);
|
|
const atLimit = val >= sp.max || (sp.key === 'serenite' && val <= sp.min);
|
|
const pill = pillInput?.closest<HTMLElement>('.dd-stat-pill');
|
|
if (pill) {
|
|
pill.classList.toggle('at-max', atLimit);
|
|
pill.style.background = atLimit ? `rgba(${sp.color},0.18)` : '';
|
|
pill.style.boxShadow = atLimit ? `0 0 8px rgba(${sp.color},0.5)` : '';
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Pill delta : pop à chaque tick — uniquement endurance, maturite, amour */
|
|
const pillDeltaStats = ['endurance', 'maturite', 'amour'];
|
|
if (pillDeltaStats.includes(def.stat)) {
|
|
const pillDelta = this.el!.querySelector<HTMLElement>(`#pill-delta-${eId}-${dId}-${def.stat}`);
|
|
if (pillDelta) {
|
|
pillDelta.textContent = r.deltaText;
|
|
if (newTick && started && !r.done) {
|
|
pillDelta.classList.remove('show');
|
|
void pillDelta.offsetWidth;
|
|
pillDelta.classList.add('show');
|
|
} else if (!started || r.done) {
|
|
pillDelta.classList.remove('show');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
/* Done badge */
|
|
const doneBadge = this.el.querySelector<HTMLElement>(`#dd-done-${eId}-${dId}`);
|
|
if (doneBadge) {
|
|
doneBadge.style.display = (allDone && started && enc.activeGauges.length > 0) ? '' : 'none';
|
|
}
|
|
|
|
/* Serenity ETA */
|
|
const serEta = this.el.querySelector(`#ser-eta-${eId}-${dId}`);
|
|
if (serEta) serEta.innerHTML = calcSerenEtaLive(enc, dd, el, started);
|
|
const serClr = this.el.querySelector<HTMLElement>(`#ser-clr-${eId}-${dId}`);
|
|
if (serClr) serClr.style.visibility = dd.sereniteTarget == null ? 'hidden' : 'visible';
|
|
|
|
/* Level ETA */
|
|
const lvlEta = this.el.querySelector(`#lvl-eta-${eId}-${dId}`);
|
|
if (lvlEta) lvlEta.innerHTML = calcLevelEtaLive(enc, dd, el, started);
|
|
const lvlClr = this.el.querySelector<HTMLElement>(`#lvl-clr-${eId}-${dId}`);
|
|
if (lvlClr) lvlClr.style.visibility = dd.levelTarget == null ? 'hidden' : 'visible';
|
|
}
|
|
|
|
destroy(): void {
|
|
this.el?.remove();
|
|
this.el = null;
|
|
}
|
|
}
|