From 26b70d29c4b4b1338fdc72d6a317b48dd0551e8c Mon Sep 17 00:00:00 2001 From: s Date: Tue, 30 Jun 2026 12:36:26 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=C3=A9cran=20Timers=20=E2=80=94=20compt?= =?UTF-8?q?e=20=C3=A0=20rebours=20automatique=20au=20kill=20de=20mob?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouvel onglet "Timers" dans le hub - Ajout de timers mob + durée (ex: 5:00 ou 90s) - Détection automatique : chaque kill du mob configuré démarre/reset le timer - Affichage des comptes à rebours avec barre de progression colorée (vert → orange → rouge) - Configs persistées en localStorage Co-Authored-By: Claude Sonnet 4.6 --- renderer/index.html | 189 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/renderer/index.html b/renderer/index.html index 6c9b393..4f9659f 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -299,6 +299,32 @@ font-family:var(--font-mono);font-size:11px;margin-bottom:5px;} .live-chart-header .chart-label{color:var(--text-dim);} .live-chart-header .chart-val{font-size:12px;font-weight:600;} + + /* ---------- Timers ---------- */ + .timer-add-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:4px;} + .timer-add-row input{background:var(--bg-panel-2);border:1px solid var(--border);color:var(--text); + font-family:var(--font-body);font-size:13px;padding:7px 10px;min-width:0;} + .timer-add-row input:focus{outline:2px solid var(--gold);outline-offset:0;} + .timer-add-row input::placeholder{color:var(--text-dim);} + .timer-config-list{display:flex;flex-direction:column;gap:6px;margin-top:12px;} + .timer-config-row{display:flex;align-items:center;gap:10px;padding:8px 10px; + background:var(--bg-panel-2);border:1px solid var(--border);} + .timer-config-mob{flex:1;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} + .timer-config-dur{font-family:var(--font-mono);font-size:12px;color:var(--text-dim);white-space:nowrap;} + .timer-active-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;margin-top:12px;} + .timer-card{background:var(--bg-panel-2);border:2px solid var(--border);padding:14px 16px;} + .timer-card.timer-ok{border-color:var(--moss);} + .timer-card.timer-warn{border-color:var(--paused);} + .timer-card.timer-done{border-color:var(--boss);background:rgba(224,85,107,.07);} + .timer-card-mob{font-size:13px;font-weight:600;margin-bottom:8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} + .timer-card-countdown{font-family:var(--font-mono);font-size:32px;font-weight:700;line-height:1;letter-spacing:.04em;} + .timer-card-countdown.timer-ok{color:var(--moss);} + .timer-card-countdown.timer-warn{color:var(--paused);} + .timer-card-countdown.timer-done{color:var(--boss);} + .timer-card-sub{font-size:11px;color:var(--text-dim);margin-top:5px;} + .timer-card-bar{height:3px;background:var(--border);margin-top:10px;overflow:hidden;} + .timer-card-bar-fill{height:100%;transition:width .9s linear;} + .timer-empty-msg{color:var(--text-dim);font-size:13px;padding:20px 0;} @@ -356,6 +382,12 @@ Statistiques des sessions passées et évolution du farm au fil du temps. ● 0 session + @@ -621,6 +653,32 @@ + + + @@ -750,7 +808,8 @@ hub: $('hubScreen'), farmTracker: $('appFarmTracker'), dropBoard: $('appDropBoard'), - history: $('appHistory') + history: $('appHistory'), + timers: $('appTimers') }; const homeBtn = $('homeBtn'); const changeFolderBtn = $('changeFolderBtn'); @@ -786,6 +845,7 @@ $('tileFarmTracker').addEventListener('click', () => showScreen('farmTracker')); $('tileDropBoard').addEventListener('click', () => { showScreen('dropBoard'); renderDropBoard(); }); $('tileHistory').addEventListener('click', () => { showScreen('history'); renderHistoryScreen(); }); + $('tileTimers').addEventListener('click', () => { showScreen('timers'); renderTimerScreen(); }); const globalState = { logPath: null, serverUrl: '', clientId: '' }; let sessionHistory = { type: 'farm-tracker-session-history', version: 1, sessions: [] }; @@ -1218,6 +1278,7 @@ state.mobCounts.set(mobPlain, (state.mobCounts.get(mobPlain) || 0) + 1); state.expTotal += exp; state.killCols += col; + triggerTimer(mobPlain); if (!state.dropStats.has(mobPlain)) state.dropStats.set(mobPlain, {kills: 0, items: new Map()}); const dropEntry = state.dropStats.get(mobPlain); @@ -1919,6 +1980,132 @@ renderHistoryScreen(); }); + // ====================================================================== + // TIMERS + // ====================================================================== + + let timerConfigs = JSON.parse(localStorage.getItem('timerConfigs') || '[]'); + // activeTimers : Map + const activeTimers = new Map(); + let timerClockId = null; + + function saveTimerConfigs(){ + localStorage.setItem('timerConfigs', JSON.stringify(timerConfigs)); + updateTimerTileStatus(); + } + + function parseDuration(str){ + str = str.trim(); + if (str.includes(':')) { + const parts = str.split(':'); + const m = parseInt(parts[0], 10) || 0; + const s = parseInt(parts[1], 10) || 0; + return m * 60 + s; + } + return parseInt(str, 10) || 0; + } + + function fmtCountdown(sec){ + if (sec <= 0) return 'PRÊT'; + const m = Math.floor(sec / 60), s = sec % 60; + return String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0'); + } + + function updateTimerTileStatus(){ + const el = $('tileTimersStatus'); + if (el) el.textContent = '● ' + timerConfigs.length + ' timer' + (timerConfigs.length !== 1 ? 's' : ''); + } + + function triggerTimer(mobPlain){ + for (const cfg of timerConfigs){ + if (cfg.mobName.toLowerCase() === mobPlain.toLowerCase()){ + const endsAt = Date.now() + cfg.durationSec * 1000; + activeTimers.set(cfg.id, { endsAt, durationSec: cfg.durationSec, mobName: cfg.mobName }); + renderTimerActiveGrid(); + } + } + } + + function startTimerClock(){ + if (timerClockId) return; + timerClockId = setInterval(() => { + if (activeTimers.size > 0) renderTimerActiveGrid(); + }, 1000); + } + + function renderTimerActiveGrid(){ + const grid = $('timerActiveGrid'); + if (!grid) return; + if (activeTimers.size === 0){ + grid.innerHTML = '

Aucun timer actif — tue un mob configuré pour démarrer un compte à rebours.

'; + return; + } + const now = Date.now(); + let html = ''; + for (const [id, timer] of activeTimers){ + const remaining = Math.max(0, Math.ceil((timer.endsAt - now) / 1000)); + const pct = Math.max(0, remaining / timer.durationSec * 100); + const cls = remaining <= 0 ? 'timer-done' : pct < 25 ? 'timer-warn' : 'timer-ok'; + const barColor = remaining <= 0 ? 'var(--boss)' : pct < 25 ? 'var(--paused)' : 'var(--moss)'; + html += + '
' + + '
'+escHtml(timer.mobName)+'
' + + '
'+fmtCountdown(remaining)+'
' + + '
'+(remaining<=0?'Respawn disponible !':'reste '+fmtCountdown(remaining))+'
'+ + '
'+ + '
'; + } + grid.innerHTML = html; + } + + function renderTimerConfigList(){ + const list = $('timerConfigList'); + if (!list) return; + if (!timerConfigs.length){ list.innerHTML = ''; return; } + list.innerHTML = timerConfigs.map(cfg => { + const m = Math.floor(cfg.durationSec/60), s = cfg.durationSec%60; + const durStr = m > 0 ? m+'min'+(s>0?' '+s+'s':'') : s+'s'; + return '
' + + ''+escHtml(cfg.mobName)+'' + + ''+escHtml(durStr)+'' + + '' + + '
'; + }).join(''); + } + + function renderTimerScreen(){ + renderTimerConfigList(); + renderTimerActiveGrid(); + startTimerClock(); + } + + $('timerAddBtn').addEventListener('click', () => { + const mobName = $('timerMobInput').value.trim(); + const durationSec = parseDuration($('timerDurInput').value); + if (!mobName || durationSec <= 0) return; + timerConfigs.push({ id: generateUUID(), mobName, durationSec }); + saveTimerConfigs(); + $('timerMobInput').value = ''; + $('timerDurInput').value = ''; + renderTimerConfigList(); + }); + + $('timerMobInput').addEventListener('keydown', e => { if (e.key === 'Enter') $('timerAddBtn').click(); }); + $('timerDurInput').addEventListener('keydown', e => { if (e.key === 'Enter') $('timerAddBtn').click(); }); + + $('timerConfigList').addEventListener('click', e => { + const id = e.target.dataset.delTimer; + if (!id) return; + timerConfigs = timerConfigs.filter(c => c.id !== id); + activeTimers.delete(id); + saveTimerConfigs(); + renderTimerConfigList(); + renderTimerActiveGrid(); + }); + + updateTimerTileStatus(); + startTimerClock(); + async function initFarmTracker(logPath, resetCounters){ if (!ft) ft = createFarmTracker(); await ft.start(logPath, resetCounters);