@@ -299,6 +299,32 @@
font-family : var ( - - font - mono ) ; font-size : 11 px ; margin-bottom : 5 px ; }
font-family : var ( - - font - mono ) ; font-size : 11 px ; margin-bottom : 5 px ; }
. live-chart-header . chart-label { color : var ( - - text - dim ) ; }
. live-chart-header . chart-label { color : var ( - - text - dim ) ; }
. live-chart-header . chart-val { font-size : 12 px ; font-weight : 600 ; }
. live-chart-header . chart-val { font-size : 12 px ; font-weight : 600 ; }
/* ---------- Timers ---------- */
. timer-add-row { display : flex ; gap : 8 px ; align-items : center ; flex-wrap : wrap ; margin-bottom : 4 px ; }
. timer-add-row input { background : var ( - - bg - panel -2 ) ; border : 1 px solid var ( - - border ) ; color : var ( - - text ) ;
font-family : var ( - - font - body ) ; font-size : 13 px ; padding : 7 px 10 px ; min-width : 0 ; }
. timer-add-row input : focus { outline : 2 px solid var ( - - gold ) ; outline-offset : 0 ; }
. timer-add-row input :: placeholder { color : var ( - - text - dim ) ; }
. timer-config-list { display : flex ; flex-direction : column ; gap : 6 px ; margin-top : 12 px ; }
. timer-config-row { display : flex ; align-items : center ; gap : 10 px ; padding : 8 px 10 px ;
background : var ( - - bg - panel -2 ) ; border : 1 px solid var ( - - border ) ; }
. timer-config-mob { flex : 1 ; font-size : 13 px ; overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ; }
. timer-config-dur { font-family : var ( - - font - mono ) ; font-size : 12 px ; color : var ( - - text - dim ) ; white-space : nowrap ; }
. timer-active-grid { display : grid ; grid-template-columns : repeat ( auto - fill , minmax ( 190 px , 1 fr ) ) ; gap : 10 px ; margin-top : 12 px ; }
. timer-card { background : var ( - - bg - panel -2 ) ; border : 2 px solid var ( - - border ) ; padding : 14 px 16 px ; }
. 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 : 13 px ; font-weight : 600 ; margin-bottom : 8 px ; overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ; }
. timer-card-countdown { font-family : var ( - - font - mono ) ; font-size : 32 px ; font-weight : 700 ; line-height : 1 ; letter-spacing : .04 em ; }
. 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 : 11 px ; color : var ( - - text - dim ) ; margin-top : 5 px ; }
. timer-card-bar { height : 3 px ; background : var ( - - border ) ; margin-top : 10 px ; overflow : hidden ; }
. timer-card-bar-fill { height : 100 % ; transition : width .9 s linear ; }
. timer-empty-msg { color : var ( - - text - dim ) ; font-size : 13 px ; padding : 20 px 0 ; }
< / style >
< / style >
< / head >
< / head >
< body >
< body >
@@ -356,6 +382,12 @@
< span class = "app-tile-desc" > Statistiques des sessions passées et évolution du farm au fil du temps.< / span >
< span class = "app-tile-desc" > Statistiques des sessions passées et évolution du farm au fil du temps.< / span >
< span class = "app-tile-status" id = "tileHistoryStatus" > ● 0 session< / span >
< span class = "app-tile-status" id = "tileHistoryStatus" > ● 0 session< / span >
< / button >
< / button >
< button class = "app-tile" id = "tileTimers" >
< span class = "app-tile-icon" > ⏱< / span >
< span class = "app-tile-title" > Timers< / span >
< span class = "app-tile-desc" > Compte à rebours automatique au kill — idéal pour les respawns de boss.< / span >
< span class = "app-tile-status" id = "tileTimersStatus" > ● 0 timer< / span >
< / button >
< / div >
< / div >
< / section >
< / section >
@@ -528,6 +560,21 @@
< p class = "hero-number" id = "histTotalTime" > 0h 00m< / p >
< p class = "hero-number" id = "histTotalTime" > 0h 00m< / p >
< p class = "hero-sub" id = "histAvgTime" > moy. 0h / session< / p >
< p class = "hero-sub" id = "histAvgTime" > moy. 0h / session< / p >
< / div >
< / div >
< div class = "hero-stat" >
< p class = "eyebrow" > Kills/min moyen< / p >
< p class = "hero-number" id = "histAvgRate" > 0< / p >
< p class = "hero-sub" id = "histBestRate" > meilleur 0/min< / p >
< / div >
< div class = "hero-stat" >
< p class = "eyebrow" > Exp totale< / p >
< p class = "hero-number" id = "histTotalExp" > 0< / p >
< p class = "hero-sub" id = "histAvgExp" > moy. 0 / session< / p >
< / div >
< div class = "hero-stat" >
< p class = "eyebrow" > Kamas totaux< / p >
< p class = "hero-number" id = "histTotalKamas" > 0< / p >
< p class = "hero-sub" id = "histAvgKamas" > moy. 0 / session< / p >
< / div >
< / div >
< / div >
< div id = "histEmptyState" >
< div id = "histEmptyState" >
@@ -552,7 +599,7 @@
< div class = "charts-2col" style = "margin-bottom:14px;" >
< div class = "charts-2col" style = "margin-bottom:14px;" >
< div class = "panel" >
< div class = "panel" >
< h2 > Kills / heure < / h2 >
< h2 > Kills / min < / h2 >
< div class = "chart-wrap" id = "chartRate" > < / div >
< div class = "chart-wrap" id = "chartRate" > < / div >
< div class = "chart-label-row" id = "chartRateLabels" > < / div >
< div class = "chart-label-row" id = "chartRateLabels" > < / div >
< / div >
< / div >
@@ -577,10 +624,13 @@
< th > Date< / th >
< th > Date< / th >
< th class = "r" > Durée< / th >
< th class = "r" > Durée< / th >
< th class = "r" > Kills< / th >
< th class = "r" > Kills< / th >
< th class = "r" > Kills/h < / th >
< th class = "r" > K/min < / th >
< th class = "r" > Exp< / th >
< th class = "r" > Exp< / th >
< th class = "r" > Cols kill < / th >
< th class = "r" > Exp/min < / th >
< th class = "r" > Kamas< / th >
< th class = "r" > Cols/min< / th >
< th > Mob principal< / th >
< th > Mob principal< / th >
< th > Top item< / th >
< / tr >
< / tr >
< / thead >
< / thead >
< tbody id = "histSessionsBody" > < / tbody >
< tbody id = "histSessionsBody" > < / tbody >
@@ -603,6 +653,32 @@
< / section >
< / section >
<!-- ============ ÉCRAN 5 : Timers ============ -->
< section class = "app-screen" id = "appTimers" hidden >
< div class = "panel" style = "margin-bottom:14px;" >
< div class = "panel-head-row" >
< h2 > Ajouter un timer< / h2 >
< / div >
< div class = "timer-add-row" >
< input type = "text" id = "timerMobInput" placeholder = "Nom du mob (ex: Skeleton)" style = "flex:2;" >
< input type = "text" id = "timerDurInput" placeholder = "Durée (ex: 5:00 ou 90)" style = "flex:1;max-width:130px;" >
< button class = "btn-ghost" id = "timerAddBtn" > Ajouter< / button >
< / div >
< div id = "timerConfigList" class = "timer-config-list" > < / div >
< / div >
< div class = "panel" >
< div class = "panel-head-row" >
< h2 > Comptes à rebours actifs< / h2 >
< div class = "spacer" > < / div >
< span style = "font-family:var(--font-mono);font-size:10px;color:var(--text-dim);" > se déclenchent automatiquement au kill< / span >
< / div >
< div id = "timerActiveGrid" class = "timer-active-grid" > < / div >
< / div >
< / section >
< / div >
< / div >
<!-- Modal 1 : confirmation sync -->
<!-- Modal 1 : confirmation sync -->
@@ -732,7 +808,8 @@
hub : $ ( 'hubScreen' ) ,
hub : $ ( 'hubScreen' ) ,
farmTracker : $ ( 'appFarmTracker' ) ,
farmTracker : $ ( 'appFarmTracker' ) ,
dropBoard : $ ( 'appDropBoard' ) ,
dropBoard : $ ( 'appDropBoard' ) ,
history : $ ( 'appHistory' )
history : $ ( 'appHistory' ) ,
timers : $ ( 'appTimers' )
} ;
} ;
const homeBtn = $ ( 'homeBtn' ) ;
const homeBtn = $ ( 'homeBtn' ) ;
const changeFolderBtn = $ ( 'changeFolderBtn' ) ;
const changeFolderBtn = $ ( 'changeFolderBtn' ) ;
@@ -768,6 +845,7 @@
$ ( 'tileFarmTracker' ) . addEventListener ( 'click' , ( ) => showScreen ( 'farmTracker' ) ) ;
$ ( 'tileFarmTracker' ) . addEventListener ( 'click' , ( ) => showScreen ( 'farmTracker' ) ) ;
$ ( 'tileDropBoard' ) . addEventListener ( 'click' , ( ) => { showScreen ( 'dropBoard' ) ; renderDropBoard ( ) ; } ) ;
$ ( 'tileDropBoard' ) . addEventListener ( 'click' , ( ) => { showScreen ( 'dropBoard' ) ; renderDropBoard ( ) ; } ) ;
$ ( 'tileHistory' ) . addEventListener ( 'click' , ( ) => { showScreen ( 'history' ) ; renderHistoryScreen ( ) ; } ) ;
$ ( 'tileHistory' ) . addEventListener ( 'click' , ( ) => { showScreen ( 'history' ) ; renderHistoryScreen ( ) ; } ) ;
$ ( 'tileTimers' ) . addEventListener ( 'click' , ( ) => { showScreen ( 'timers' ) ; renderTimerScreen ( ) ; } ) ;
const globalState = { logPath : null , serverUrl : '' , clientId : '' } ;
const globalState = { logPath : null , serverUrl : '' , clientId : '' } ;
let sessionHistory = { type : 'farm-tracker-session-history' , version : 1 , sessions : [ ] } ;
let sessionHistory = { type : 'farm-tracker-session-history' , version : 1 , sessions : [ ] } ;
@@ -1200,6 +1278,7 @@
state . mobCounts . set ( mobPlain , ( state . mobCounts . get ( mobPlain ) || 0 ) + 1 ) ;
state . mobCounts . set ( mobPlain , ( state . mobCounts . get ( mobPlain ) || 0 ) + 1 ) ;
state . expTotal += exp ;
state . expTotal += exp ;
state . killCols += col ;
state . killCols += col ;
triggerTimer ( mobPlain ) ;
if ( ! state . dropStats . has ( mobPlain ) ) state . dropStats . set ( mobPlain , { kills : 0 , items : new Map ( ) } ) ;
if ( ! state . dropStats . has ( mobPlain ) ) state . dropStats . set ( mobPlain , { kills : 0 , items : new Map ( ) } ) ;
const dropEntry = state . dropStats . get ( mobPlain ) ;
const dropEntry = state . dropStats . get ( mobPlain ) ;
@@ -1834,8 +1913,15 @@
histEmpty . hidden = true ; histHero . hidden = false ; histCharts . hidden = false ; histPanel . hidden = false ;
histEmpty . hidden = true ; histHero . hidden = false ; histCharts . hidden = false ; histPanel . hidden = false ;
const totalKills = sessions . reduce ( ( s , x ) => s + x . totalKills , 0 ) ;
const totalKills = sessions . reduce ( ( s , x ) => s + x . totalKills , 0 ) ;
const totalMs = sessions . reduce ( ( s , x ) => s + x . durationMs , 0 ) ;
const totalMs = sessions . reduce ( ( s , x ) => s + x . durationMs , 0 ) ;
const totalExp = sessions . reduce ( ( s , x ) => s + ( x . totalExp || 0 ) , 0 ) ;
const totalKamas = sessions . reduce ( ( s , x ) => s + ( x . sellTotal || 0 ) , 0 ) ;
const avgKills = Math . round ( totalKills / sessions . length ) ;
const avgKills = Math . round ( totalKills / sessions . length ) ;
const avgMs = totalMs / sessions . length ;
const avgMs = totalMs / sessions . length ;
const avgExp = Math . round ( totalExp / sessions . length ) ;
const avgKamas = Math . round ( totalKamas / sessions . length ) ;
const ratesPerMin = sessions . filter ( s => s . durationMs > 0 ) . map ( s => s . totalKills / ( s . durationMs / 60000 ) ) ;
const avgRate = ratesPerMin . length ? ratesPerMin . reduce ( ( a , b ) => a + b , 0 ) / ratesPerMin . length : 0 ;
const bestRate = ratesPerMin . length ? Math . max ( ... ratesPerMin ) : 0 ;
$ ( 'histTotalSessions' ) . textContent = fmt ( sessions . length ) ;
$ ( 'histTotalSessions' ) . textContent = fmt ( sessions . length ) ;
$ ( 'histTotalKills' ) . textContent = fmt ( totalKills ) ;
$ ( 'histTotalKills' ) . textContent = fmt ( totalKills ) ;
$ ( 'histAvgKills' ) . textContent = 'moy. ' + fmt ( avgKills ) + ' / session' ;
$ ( 'histAvgKills' ) . textContent = 'moy. ' + fmt ( avgKills ) + ' / session' ;
@@ -1843,12 +1929,18 @@
$ ( 'histTotalTime' ) . textContent = tH + 'h ' + String ( tM ) . padStart ( 2 , '0' ) + 'm' ;
$ ( 'histTotalTime' ) . textContent = tH + 'h ' + String ( tM ) . padStart ( 2 , '0' ) + 'm' ;
const aH = Math . floor ( avgMs / 3600000 ) , aM = Math . floor ( ( avgMs % 3600000 ) / 60000 ) ;
const aH = Math . floor ( avgMs / 3600000 ) , aM = Math . floor ( ( avgMs % 3600000 ) / 60000 ) ;
$ ( 'histAvgTime' ) . textContent = 'moy. ' + ( aH > 0 ? aH + 'h ' : '' ) + String ( aM ) . padStart ( 2 , '0' ) + 'm / session' ;
$ ( 'histAvgTime' ) . textContent = 'moy. ' + ( aH > 0 ? aH + 'h ' : '' ) + String ( aM ) . padStart ( 2 , '0' ) + 'm / session' ;
$ ( 'histAvgRate' ) . textContent = fmt ( avgRate , 1 ) + '/min' ;
$ ( 'histBestRate' ) . textContent = 'meilleur ' + fmt ( bestRate , 1 ) + '/min' ;
$ ( 'histTotalExp' ) . textContent = fmtShort ( totalExp ) ;
$ ( 'histAvgExp' ) . textContent = 'moy. ' + fmtShort ( avgExp ) + ' / session' ;
$ ( 'histTotalKamas' ) . textContent = fmtShort ( totalKamas ) ;
$ ( 'histAvgKamas' ) . textContent = 'moy. ' + fmtShort ( avgKamas ) + ' / session' ;
const kAvgEl = $ ( 'histKillsAvgLabel' ) ;
const kAvgEl = $ ( 'histKillsAvgLabel' ) ;
if ( kAvgEl ) kAvgEl . textContent = 'moy. ' + fmt ( avgKills ) + ' kills' ;
if ( kAvgEl ) kAvgEl . textContent = 'moy. ' + fmt ( avgKills ) + ' kills' ;
renderSvgChart ( $ ( 'chartKills' ) , sessions , s => s . totalKills , '#9B5DE5' , null , $ ( 'chartKillsLabels' ) ) ;
renderSvgChart ( $ ( 'chartKills' ) , sessions , s => s . totalKills , '#9B5DE5' , null , $ ( 'chartKillsLabels' ) ) ;
renderSvgChart ( $ ( 'chartRate' ) , sessions , s => {
renderSvgChart ( $ ( 'chartRate' ) , sessions , s => {
const h = s . durationMs / 360 0000; return h > 0 ? Math . round ( s . totalKills / h ) : 0 ;
const min = s . durationMs / 6 0000; return min > 0 ? Math . round ( s . totalKills / min * 10 ) / 10 : 0 ;
} , '#5DB7E5' , v => fmtShort ( v ) + '/h ' , $ ( 'chartRateLabels' ) ) ;
} , '#5DB7E5' , v => fmt ( v , 1 )+ '/min ' , $ ( 'chartRateLabels' ) ) ;
renderSvgChart ( $ ( 'chartExp' ) , sessions , s => s . totalExp , '#F2C94C' , v => fmtShort ( v ) , $ ( 'chartExpLabels' ) ) ;
renderSvgChart ( $ ( 'chartExp' ) , sessions , s => s . totalExp , '#F2C94C' , v => fmtShort ( v ) , $ ( 'chartExpLabels' ) ) ;
const tbody = $ ( 'histSessionsBody' ) ;
const tbody = $ ( 'histSessionsBody' ) ;
tbody . innerHTML = '' ;
tbody . innerHTML = '' ;
@@ -1859,16 +1951,23 @@
+ ' ' + d . toLocaleTimeString ( 'fr-FR' , { hour : '2-digit' , minute : '2-digit' } ) ;
+ ' ' + d . toLocaleTimeString ( 'fr-FR' , { hour : '2-digit' , minute : '2-digit' } ) ;
const dH = Math . floor ( s . durationMs / 3600000 ) , dM = Math . floor ( ( s . durationMs % 3600000 ) / 60000 ) ;
const dH = Math . floor ( s . durationMs / 3600000 ) , dM = Math . floor ( ( s . durationMs % 3600000 ) / 60000 ) ;
const durStr = dH > 0 ? dH + 'h' + String ( dM ) . padStart ( 2 , '0' ) + 'm' : dM + 'min' ;
const durStr = dH > 0 ? dH + 'h' + String ( dM ) . padStart ( 2 , '0' ) + 'm' : dM + 'min' ;
const killsPerH = s . durationMs > 0 ? Math . round ( s . totalKills / ( s . durationMs / 3600000 ) ) : 0;
const minElapsed = s . durationMs / 6000 0;
const killsPerMin = minElapsed > 0 ? Math . round ( s . totalKills / minElapsed * 10 ) / 10 : 0 ;
const expPerMin = minElapsed > 0 ? Math . round ( s . totalExp / minElapsed ) : 0 ;
const colsPerMin = minElapsed > 0 ? Math . round ( ( s . killCols || 0 ) / minElapsed * 10 ) / 10 : 0 ;
const topItemName = s . topItems && s . topItems . length ? s . topItems [ 0 ] . name + ' × ' + s . topItems [ 0 ] . qty : '—' ;
const tr = document . createElement ( 'tr' ) ;
const tr = document . createElement ( 'tr' ) ;
tr . innerHTML =
tr . innerHTML =
'<td>' + escHtml ( dateStr ) + '</td>' +
'<td>' + escHtml ( dateStr ) + '</td>' +
'<td class="r mono">' + escHtml ( durStr ) + '</td>' +
'<td class="r mono">' + escHtml ( durStr ) + '</td>' +
'<td class="r hl">' + fmt ( s . totalKills ) + '</td>' +
'<td class="r hl">' + fmt ( s . totalKills ) + '</td>' +
'<td class="r mono">' + fmt ( killsPerH ) + '/h </td>' +
'<td class="r mono">' + fmt ( killsPerMin , 1 ) + '/min </td>' +
'<td class="r mono">' + fmt ( s . totalExp ) + '</td>' +
'<td class="r mono">' + fmtShort ( s . totalExp ) + '</td>' +
'<td class="r mono">' + fmt( s . killCols ) + '</td>' +
'<td class="r mono">' + fmtShort ( expPerMin ) + '/min </td>' +
'<td class="dim ">' + escHtml ( s . topMob || '—' ) + '</td>' ;
'<td class="r mono ">' + ( s . sellTotal ? fmtShort ( Math . round ( s . sellTotal ) ) : '—' ) + '</td>' +
'<td class="r mono">' + fmt ( colsPerMin , 1 ) + '/min</td>' +
'<td class="dim">' + escHtml ( s . topMob || '—' ) + '</td>' +
'<td class="dim" title="' + escHtml ( topItemName ) + '">' + escHtml ( topItemName . length > 22 ? topItemName . slice ( 0 , 20 ) + '…' : topItemName ) + '</td>' ;
tbody . appendChild ( tr ) ;
tbody . appendChild ( tr ) ;
}
}
}
}
@@ -1881,6 +1980,132 @@
renderHistoryScreen ( ) ;
renderHistoryScreen ( ) ;
} ) ;
} ) ;
// ======================================================================
// TIMERS
// ======================================================================
let timerConfigs = JSON . parse ( localStorage . getItem ( 'timerConfigs' ) || '[]' ) ;
// activeTimers : Map<configId, { endsAt: number, durationSec: number, mobName: string }>
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 = '<p class="timer-empty-msg">Aucun timer actif — tue un mob configuré pour démarrer un compte à rebours.</p>' ;
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 +=
'<div class="timer-card ' + cls + '" data-timer-id="' + escHtml ( id ) + '">' +
'<div class="timer-card-mob">' + escHtml ( timer . mobName ) + '</div>' +
'<div class="timer-card-countdown ' + cls + '">' + fmtCountdown ( remaining ) + '</div>' +
'<div class="timer-card-sub">' + ( remaining <= 0 ? 'Respawn disponible !' : 'reste ' + fmtCountdown ( remaining ) ) + '</div>' +
'<div class="timer-card-bar"><div class="timer-card-bar-fill" style="width:' + pct . toFixed ( 1 ) + '%;background:' + barColor + ';"></div></div>' +
'</div>' ;
}
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 '<div class="timer-config-row">' +
'<span class="timer-config-mob">' + escHtml ( cfg . mobName ) + '</span>' +
'<span class="timer-config-dur">' + escHtml ( durStr ) + '</span>' +
'<button class="btn-ghost small danger" data-del-timer="' + escHtml ( cfg . id ) + '">✕</button>' +
'</div>' ;
} ) . 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 ) {
async function initFarmTracker ( logPath , resetCounters ) {
if ( ! ft ) ft = createFarmTracker ( ) ;
if ( ! ft ) ft = createFarmTracker ( ) ;
await ft . start ( logPath , resetCounters ) ;
await ft . start ( logPath , resetCounters ) ;