2026-06-22 16:41:53 +00:00
// retoor <retoor@molodetz.nl>
2026-07-26 14:57:18 +02:00
import { Format } from "./Format.js" ;
2026-06-22 16:41:53 +00:00
import { Http } from "./Http.js" ;
import { Poller } from "./Poller.js" ;
export class GameFarm {
constructor () {
this . root = document . querySelector ( "[data-game-root]" );
if ( ! this . root ) return ;
this . mode = this . root . dataset . mode || "own" ;
this . username = this . root . dataset . username || "" ;
this . stateUrl = this . mode === "view" ? `/game/farm/ ${ this . username } ` : "/game/state" ;
this . farm = null ;
2026-07-21 03:36:29 +02:00
this . board = "score" ;
2026-06-22 16:41:53 +00:00
this . _bindActions ();
2026-07-21 03:36:29 +02:00
this . _bindLeaderboardBoard ();
2026-06-22 16:41:53 +00:00
this . _startTicker ();
this . load ();
if ( this . mode === "own" ) this . _loadLeaderboard ();
if ( window . app && window . app . pubsub && this . username ) {
window . app . pubsub . subscribe ( `public.game.farm. ${ this . username } ` , () => this . load ());
}
this . poller = new Poller (() => this . load (), 20000 , { pauseHidden : true , immediate : false });
}
async load () {
try {
const data = await Http . getJson ( this . stateUrl );
this . render ( data . farm );
} catch ( error ) {
return ;
}
}
_bindActions () {
this . root . addEventListener ( "submit" , ( event ) => {
const form = event . target . closest ( "form[data-game-action]" );
if ( ! form ) return ;
event . preventDefault ();
this . _submit ( form );
});
}
2026-07-21 03:36:29 +02:00
_bindLeaderboardBoard () {
const select = this . root . querySelector ( "[data-lb-board]" );
if ( ! select ) return ;
select . addEventListener ( "change" , () => {
this . board = select . value ;
this . _loadLeaderboard ();
});
}
2026-06-22 16:41:53 +00:00
async _submit ( form ) {
const action = form . dataset . gameAction ;
const params = {};
form . querySelectorAll ( "[name]" ). forEach (( field ) => {
params [ field . name ] = field . value ;
});
const button = form . querySelector ( "button" );
if ( button ) button . disabled = true ;
try {
const data = await Http . send ( form . getAttribute ( "action" ), params );
this . render ( data . farm );
2026-06-22 17:33:03 +00:00
if ( action === "steal" && window . app && window . app . toast ) {
2026-07-26 14:57:18 +02:00
window . app . toast . show ( `You stole ${ Format . coins ( data . stole_coins ) } !` , { type : "success" });
2026-06-22 17:33:03 +00:00
}
if ( action === "harvest" || action === "water" || action === "steal" || action === "prestige" ) {
2026-06-22 16:41:53 +00:00
this . _loadLeaderboard ();
}
} catch ( error ) {
if ( button ) button . disabled = false ;
}
}
render ( farm ) {
if ( ! farm ) return ;
this . farm = farm ;
this . _renderHud ( farm );
this . _setHost ( "[data-game-grid-host]" , this . _gridHtml ( farm ));
if ( this . mode === "own" ) {
this . _setHost ( "[data-shop-host]" , this . _shopHtml ( farm ));
2026-07-21 03:36:29 +02:00
this . _setHost ( "[data-defense-host]" , this . _defenseHtml ( farm ));
this . _setHost ( "[data-infra-host]" , this . _infraHtml ( farm ));
2026-06-22 16:41:53 +00:00
this . _setHost ( "[data-perk-host]" , this . _perksHtml ( farm ));
2026-06-27 22:31:34 +00:00
this . _setHost ( "[data-legacy-host]" , this . _legacyHtml ( farm ));
2026-07-21 03:36:29 +02:00
this . _setHost ( "[data-mastery-host]" , this . _masteryHtml ( farm ));
this . _setHost ( "[data-analytics-host]" , this . _analyticsHtml ( farm ));
this . _setHost ( "[data-cosmetics-host]" , this . _cosmeticsHtml ( farm ));
2026-06-22 16:41:53 +00:00
this . _setHost ( "[data-daily-host]" , this . _dailyHtml ( farm ));
2026-07-23 01:14:10 +02:00
this . _setHost ( "[data-grant-host]" , this . _grantHtml ( farm ));
2026-06-22 16:41:53 +00:00
this . _setHost ( "[data-quest-host]" , this . _questsHtml ( farm ));
}
this . _tick ();
}
_setHost ( selector , html ) {
const host = this . root . querySelector ( selector );
if ( host ) host . innerHTML = html ;
}
_renderHud ( farm ) {
const set = ( selector , value ) => {
const node = this . root . querySelector ( selector );
if ( node ) node . textContent = value ;
};
2026-07-26 14:57:18 +02:00
this . _setCoins ( "[data-hud-coins]" , farm . coins );
2026-06-22 16:41:53 +00:00
set ( "[data-hud-level]" , farm . level );
set ( "[data-hud-ci]" , farm . ci_tier );
2026-07-26 14:57:18 +02:00
set ( "[data-hud-ci-label]" , ` ${ farm . ci_label } ( ${ Format . speed ( farm . ci_speed ) } )` );
set ( "[data-hud-harvests]" , Format . exact ( farm . total_harvests ));
2026-06-22 16:41:53 +00:00
set ( "[data-hud-prestige]" , farm . prestige );
set ( "[data-hud-prestige-mult]" , `+ ${ Math . round ( farm . prestige_multiplier * 100 - 100 ) } % coins` );
2026-07-26 14:57:18 +02:00
set ( "[data-hud-stars]" , Format . exact ( farm . stars ));
set ( "[data-hud-mastery]" , Format . exact ( farm . mastery_points ));
set ( "[data-era-coins]" , Format . coins ( farm . era_coins ));
set ( "[data-era-harvests]" , Format . exact ( farm . era_harvests ));
2026-06-22 16:41:53 +00:00
const fill = this . root . querySelector ( "[data-hud-xp-fill]" );
if ( fill ) {
const pct = farm . level_is_max || ! farm . level_span ? 100 : Math . floor (( 100 * farm . level_into ) / farm . level_span );
2026-06-22 23:13:48 +00:00
fill . style . setProperty ( "--hud-xp" , ` ${ pct } %` );
2026-06-22 16:41:53 +00:00
}
2026-07-26 14:57:18 +02:00
set ( "[data-hud-xp]" , farm . level_is_max ? "max level" : ` ${ Format . exact ( farm . level_into ) } / ${ Format . exact ( farm . level_span ) } XP` );
2026-06-22 16:41:53 +00:00
}
_shopHtml ( farm ) {
const ciUpgrade = farm . ci_next_tier
2026-07-26 14:57:18 +02:00
? `<form method="post" action="/game/upgrade" data-game-action="upgrade"><button type="submit" class="btn btn-sm btn-primary">Upgrade ( ${ Format . coins ( farm . ci_next_cost ) } )</button></form>`
2026-06-22 16:41:53 +00:00
: "" ;
const ciText = farm . ci_next_tier
? `Upgrade to ${ farm . ci_next_label } for faster builds`
: "CI is at the top tier." ;
2026-07-23 01:14:10 +02:00
const prestigeBtn = farm . refactor_affordable
2026-07-26 14:57:18 +02:00
? `<form method="post" action="/game/prestige" data-game-action="prestige"><button type="submit" class="btn btn-sm btn-primary" data-confirm="Refactoring costs ${ Format . exact ( farm . refactor_cost ) } coins and resets coins, level, CI, extra plots, and perks for a permanent +25% coin bonus. You carry over ${ Format . exact ( farm . refactor_carryover_preview ) } coins and keep your Stars and Legacy upgrades. Continue?">Refactor ( ${ Format . coins ( farm . refactor_cost ) } )</button></form>`
2026-06-22 16:41:53 +00:00
: "" ;
2026-07-23 01:14:10 +02:00
let prestigeText ;
if ( ! farm . prestige_available ) {
prestigeText = `Reach level ${ farm . prestige_min_level } to refactor.` ;
} else if ( farm . refactor_affordable ) {
2026-07-26 14:57:18 +02:00
prestigeText = `Pay ${ Format . coins ( farm . refactor_cost ) } to reset your farm for a permanent +25% coin bonus. You keep ${ farm . refactor_carryover_pct } % of what remains ( ${ Format . coins ( farm . refactor_carryover_preview ) } ) plus your Stars and Legacy upgrades.` ;
2026-07-23 01:14:10 +02:00
} else {
2026-07-26 14:57:18 +02:00
prestigeText = `Refactoring costs ${ Format . coins ( farm . refactor_cost ) } right now - keep farming to afford it.` ;
2026-07-23 01:14:10 +02:00
}
2026-06-22 16:41:53 +00:00
return (
`<div class="shop-row"><div class="shop-info"><strong>CI upgrade</strong><span> ${ ciText } </span></div> ${ ciUpgrade } </div>` +
`<div class="shop-row"><div class="shop-info"><strong>Refactor (prestige ${ farm . prestige } )</strong><span> ${ prestigeText } </span></div> ${ prestigeBtn } </div>`
);
}
_perksHtml ( farm ) {
return ( farm . perks || [])
. map (( perk ) => {
const action = perk . maxed
? `<span class="perk-maxed">Maxed</span>`
2026-07-26 14:57:18 +02:00
: `<form method="post" action="/game/perk" data-game-action="perk"><input type="hidden" name="perk" value=" ${ perk . key } "><button type="submit" class="btn btn-sm">Upgrade ( ${ Format . coins ( perk . cost ) } )</button></form>` ;
2026-06-22 16:41:53 +00:00
return `<div class="perk-card"><span class="perk-icon" aria-hidden="true"> ${ perk . icon } </span><strong class="perk-name"> ${ perk . name } </strong><span class="perk-level">Lv ${ perk . level } / ${ perk . max_level } </span><span class="perk-effect"> ${ perk . effect || perk . description } </span> ${ action } </div>` ;
})
. join ( "" );
}
2026-06-27 22:31:34 +00:00
_legacyHtml ( farm ) {
return ( farm . legacy || [])
. map (( up ) => {
const action = up . maxed
? `<span class="perk-maxed">Maxed</span>`
2026-07-26 14:57:18 +02:00
: `<form method="post" action="/game/legacy" data-game-action="legacy"><input type="hidden" name="key" value=" ${ up . key } "><button type="submit" class="btn btn-sm">Buy ( ${ Format . exact ( up . cost ) } ★)</button></form>` ;
2026-06-27 22:31:34 +00:00
return `<div class="perk-card legacy-card"><span class="perk-icon" aria-hidden="true"> ${ up . icon } </span><strong class="perk-name"> ${ up . name } </strong><span class="perk-level">Lv ${ up . level } / ${ up . max_level } </span><span class="perk-effect"> ${ up . effect || up . description } </span> ${ action } </div>` ;
})
. join ( "" );
}
2026-07-21 03:36:29 +02:00
_masteryHtml ( farm ) {
return ( farm . mastery || [])
. map (( m ) => {
const action = m . maxed
? `<span class="perk-maxed">Maxed</span>`
2026-07-26 14:57:18 +02:00
: `<form method="post" action="/game/mastery" data-game-action="mastery"><input type="hidden" name="key" value=" ${ m . key } "><button type="submit" class="btn btn-sm">Upgrade ( ${ Format . exact ( m . cost ) } Mastery)</button></form>` ;
2026-07-21 03:36:29 +02:00
return `<div class="perk-card"><span class="perk-icon" aria-hidden="true"> ${ m . icon } </span><strong class="perk-name"> ${ m . name } </strong><span class="perk-level">Lv ${ m . level } / ${ m . max_level } </span><span class="perk-effect"> ${ m . effect || m . description } </span> ${ action } </div>` ;
})
. join ( "" );
}
_analyticsHtml ( farm ) {
if ( ! farm . mastery_analytics_unlocked ) return "" ;
return (
2026-07-26 14:57:18 +02:00
`<div class="shop-row"><div class="shop-info"><strong>Lifetime coins earned</strong><span> ${ Format . coins ( farm . lifetime_coins_earned ) } since Farm Analytics was unlocked.</span></div></div>` +
`<div class="shop-row"><div class="shop-info"><strong>Lifetime harvests</strong><span> ${ Format . exact ( farm . lifetime_harvests ) } builds harvested since Farm Analytics was unlocked.</span></div></div>`
2026-07-21 03:36:29 +02:00
);
}
_infraHtml ( farm ) {
return ( farm . infrastructure || [])
. map (( infra ) => {
let action ;
if ( infra . owned ) {
action = `<span class="perk-maxed">Owned</span>` ;
} else if ( farm . prestige < infra . min_prestige ) {
action = `<span class="perk-maxed">Requires prestige ${ infra . min_prestige } </span>` ;
} else {
2026-07-26 14:57:18 +02:00
action = `<form method="post" action="/game/infrastructure/buy" data-game-action="infrastructure"><input type="hidden" name="key" value=" ${ infra . key } "><button type="submit" class="btn btn-sm" data-confirm="Buy ${ infra . name } for ${ Format . exact ( infra . cost ) } coins?">Buy ( ${ Format . coins ( infra . cost ) } )</button></form>` ;
2026-07-21 03:36:29 +02:00
}
return `<div class="perk-card"><span class="perk-icon" aria-hidden="true"> ${ infra . icon } </span><strong class="perk-name"> ${ infra . name } </strong><span class="perk-effect"> ${ infra . description } </span> ${ action } </div>` ;
})
. join ( "" );
}
_defenseHtml ( farm ) {
const upkeep = farm . defense_upkeep_daily
2026-07-26 14:57:18 +02:00
? `Costs ${ Format . coins ( farm . defense_upkeep_daily ) } /day upkeep. If you cannot pay you lose one level - never your whole balance.`
2026-07-21 03:36:29 +02:00
: "No upkeep at level 0." ;
const action = farm . defense_next_cost
2026-07-26 14:57:18 +02:00
? `<form method="post" action="/game/defense/upgrade" data-game-action="defense"><button type="submit" class="btn btn-sm btn-primary">Upgrade ( ${ Format . coins ( farm . defense_next_cost ) } )</button></form>`
2026-07-21 03:36:29 +02:00
: `<span class="perk-maxed">Maxed</span>` ;
2026-07-26 14:57:18 +02:00
const downgrade = farm . defense_downgrade_available
? `<form method="post" action="/game/defense/downgrade" data-game-action="defense-downgrade"><button type="submit" class="btn btn-sm" data-confirm="Drop to the next defense tier down? Lower upkeep, less protection.">Downgrade</button></form>`
: "" ;
return `<div class="shop-row"><div class="shop-info"><strong>Defense: ${ farm . defense_tier_name } (level ${ farm . defense_level } )</strong><span> ${ upkeep } </span></div><div class="shop-actions"> ${ action }${ downgrade } </div></div>` ;
2026-07-21 03:36:29 +02:00
}
_cosmeticsHtml ( farm ) {
return ( farm . cosmetics || [])
. map (( c ) => {
let action ;
if ( c . owned && c . kind === "title" ) {
action =
farm . active_title === c . key
? `<span class="perk-maxed">Equipped</span>`
: `<form method="post" action="/game/cosmetics/equip" data-game-action="cosmetic-equip"><input type="hidden" name="key" value=" ${ c . key } "><button type="submit" class="btn btn-sm">Equip</button></form>` ;
} else if ( c . owned ) {
action = `<span class="perk-maxed">Owned</span>` ;
} else {
2026-07-26 14:57:18 +02:00
action = `<form method="post" action="/game/cosmetics/buy" data-game-action="cosmetic-buy"><input type="hidden" name="key" value=" ${ c . key } "><button type="submit" class="btn btn-sm" data-confirm="Buy ${ c . name } for ${ Format . exact ( c . cost_coins ) } coins?">Buy ( ${ Format . coins ( c . cost_coins ) } )</button></form>` ;
2026-07-21 03:36:29 +02:00
}
return `<div class="perk-card"><span class="perk-icon" aria-hidden="true"> ${ c . icon } </span><strong class="perk-name"> ${ c . name } </strong><span class="perk-effect"> ${ c . description } </span> ${ action } </div>` ;
})
. join ( "" );
}
2026-07-23 01:14:10 +02:00
_grantHtml ( farm ) {
const action = farm . grant_available
2026-07-26 14:57:18 +02:00
? `<form method="post" action="/game/grant" data-game-action="grant"><button type="submit" class="btn btn-sm btn-primary">Claim + ${ Format . coins ( farm . grant_amount ) } </button></form>`
2026-07-23 01:14:10 +02:00
: `<span class="daily-claimed"> ${ farm . grant_reason } </span>` ;
2026-07-26 14:57:18 +02:00
return `<p class="daily-streak">Treasury: <strong> ${ Format . coins ( farm . treasury_balance ) } </strong> collected from refactor fees</p> ${ action } ` ;
2026-07-23 01:14:10 +02:00
}
2026-06-22 16:41:53 +00:00
_dailyHtml ( farm ) {
const action = farm . daily_available
2026-07-26 14:57:18 +02:00
? `<form method="post" action="/game/daily" data-game-action="daily"><button type="submit" class="btn btn-sm btn-primary">Claim + ${ Format . coins ( farm . daily_reward ) } </button></form>`
2026-06-22 16:41:53 +00:00
: `<span class="daily-claimed">Claimed today. Come back tomorrow.</span>` ;
2026-07-26 14:57:18 +02:00
return `<p class="daily-streak">Streak: <strong> ${ Format . exact ( farm . streak ) } </strong> day(s) ${ farm . daily_streak_reset ? " - your streak lapsed, claiming today starts a new one" : "" } </p> ${ action } ` ;
2026-06-22 16:41:53 +00:00
}
_questsHtml ( farm ) {
const quests = farm . quests || [];
if ( ! quests . length ) return `<li class="game-lb-empty">No quests today.</li>` ;
return quests
. map (( quest ) => {
const pct = quest . goal ? Math . floor (( 100 * quest . progress ) / quest . goal ) : 0 ;
2026-07-26 14:57:18 +02:00
const reward = quest . reward_stars ? `+ ${ Format . exact ( quest . reward_stars ) } ★` : `+ ${ Format . coins ( quest . reward_coins ) } ` ;
2026-06-22 16:41:53 +00:00
let foot = "" ;
if ( quest . can_claim ) {
2026-07-21 03:36:29 +02:00
foot = `<form method="post" action="/game/quests/claim" data-game-action="claim-quest"><input type="hidden" name="quest" value=" ${ quest . kind } "><input type="hidden" name="scope" value=" ${ quest . scope } "><button type="submit" class="btn btn-sm btn-primary">Claim</button></form>` ;
2026-06-22 16:41:53 +00:00
} else if ( quest . claimed ) {
foot = `<span class="quest-claimed">Done</span>` ;
}
2026-07-26 14:57:18 +02:00
return `<li class="quest-item ${ quest . claimed ? " quest-done" : "" } "><div class="quest-head"><span> ${ quest . label } </span><span class="quest-reward"> ${ reward } </span></div><div class="quest-bar"><span class="quest-fill" style="--quest-pct: ${ pct } %"></span></div><div class="quest-foot"><span class="quest-progress"> ${ Format . exact ( quest . progress ) } / ${ Format . exact ( quest . goal ) } </span> ${ foot } </div></li>` ;
2026-06-22 16:41:53 +00:00
})
. join ( "" );
}
_gridHtml ( farm ) {
const cells = farm . plots . map (( plot ) => this . _plotHtml ( plot , farm )). join ( "" );
let buy = "" ;
if ( farm . is_owner && farm . plot_count < farm . max_plots ) {
2026-07-26 14:57:18 +02:00
buy = `<form class="game-plot game-plot-buy" method="post" action="/game/buy-plot" data-game-action="buy-plot"><button type="submit" class="btn btn-sm">+ New plot ( ${ Format . coins ( farm . next_plot_cost ) } )</button></form>` ;
2026-06-22 16:41:53 +00:00
}
return `<div class="game-grid" data-game-grid> ${ cells }${ buy } </div>` ;
}
_plotHtml ( plot , farm ) {
2026-06-27 22:31:34 +00:00
const golden = plot . is_golden ? " game-plot-golden" : "" ;
const head = `<div class="game-plot game-plot- ${ plot . state }${ golden } " data-slot=" ${ plot . slot } " data-state=" ${ plot . state } " data-ready-at=" ${ plot . ready_at } ">` ;
2026-06-22 16:41:53 +00:00
let body = "" ;
if ( plot . state === "empty" ) {
if ( farm . is_owner ) {
const options = farm . crops
. map (( crop ) => {
const disabled = crop . locked || crop . cost > farm . coins ? " disabled" : "" ;
const lock = crop . locked ? ` (Lv ${ crop . min_level } )` : "" ;
2026-07-21 03:36:29 +02:00
const market =
crop . market_state === "saturated"
? " (saturated)"
: crop . market_state === "boosted"
? " (boosted)"
: "" ;
2026-07-26 14:57:18 +02:00
return `<option value=" ${ crop . key } " ${ disabled } > ${ crop . icon } ${ crop . name } - ${ Format . coins ( crop . cost ) }${ lock }${ market } </option>` ;
2026-06-22 16:41:53 +00:00
})
. join ( "" );
body = `<form class="plot-plant-form" method="post" action="/game/plant" data-game-action="plant"><input type="hidden" name="slot" value=" ${ plot . slot } "><select name="crop" class="plot-crop-select" aria-label="Choose a project to build"> ${ options } </select><button type="submit" class="btn btn-sm btn-primary">Plant</button></form>` ;
} else {
body = `<span class="plot-empty-label">Empty plot</span>` ;
}
} else if ( plot . state === "growing" ) {
body = `<span class="plot-icon" aria-hidden="true"> ${ plot . crop_icon } </span><span class="plot-crop-name"> ${ plot . crop_name } </span><span class="plot-timer" data-plot-timer data-ready-at=" ${ plot . ready_at } ">building</span><span class="plot-water-count"> ${ plot . watered_count } / ${ plot . max_waters } 💧</span>` ;
if ( plot . can_water ) {
body += `<form method="post" action="/game/farm/ ${ farm . owner_username } /water" data-game-action="water"><input type="hidden" name="slot" value=" ${ plot . slot } "><button type="submit" class="btn btn-sm">💧 Water</button></form>` ;
}
if ( farm . is_owner && plot . fertilize_cost ) {
2026-07-26 14:57:18 +02:00
body += `<form method="post" action="/game/fertilize" data-game-action="fertilize"><input type="hidden" name="slot" value=" ${ plot . slot } "><button type="submit" class="btn btn-sm">⚡ Fertilize ( ${ Format . coins ( plot . fertilize_cost ) } )</button></form>` ;
2026-06-22 16:41:53 +00:00
}
} else {
2026-06-27 22:31:34 +00:00
const goldenMark = plot . is_golden ? " ✨" : "" ;
2026-07-26 14:57:18 +02:00
const raided = plot . raided_pct ? `<span class="plot-raided">Raided ${ plot . raided_pct } %</span>` : "" ;
body = `<span class="plot-icon plot-ready-icon" aria-hidden="true"> ${ plot . crop_icon } </span><span class="plot-crop-name"> ${ plot . crop_name }${ goldenMark } </span> ${ raided } ` ;
2026-06-22 16:41:53 +00:00
if ( farm . is_owner ) {
2026-06-27 22:31:34 +00:00
const label = plot . is_golden ? "Harvest golden" : "Harvest" ;
body += `<form method="post" action="/game/harvest" data-game-action="harvest"><input type="hidden" name="slot" value=" ${ plot . slot } "><button type="submit" class="btn btn-sm btn-primary"> ${ label } </button></form>` ;
2026-06-22 17:33:03 +00:00
} else if ( plot . can_steal ) {
2026-07-26 14:57:18 +02:00
body += `<form method="post" action="/game/farm/ ${ farm . owner_username } /steal" data-game-action="steal"><input type="hidden" name="slot" value=" ${ plot . slot } "><button type="submit" class="btn btn-sm btn-danger" data-confirm="Steal this ready build?">Steal + ${ Format . coins ( plot . steal_coins ) } </button></form>` ;
} else if ( plot . steal_reason === "stripped" ) {
body += `<span class="plot-ready-label plot-steal-locked">Already stripped</span>` ;
2026-06-27 22:31:34 +00:00
} else if ( plot . steal_reason === "cooldown" ) {
body += `<span class="plot-ready-label plot-steal-locked">Raid again in ${ this . _format ( plot . steal_cooldown_seconds ) } </span>` ;
2026-06-22 16:41:53 +00:00
} else {
body += `<span class="plot-ready-label">Ready</span>` ;
}
}
return ` ${ head }${ body } </div>` ;
}
_startTicker () {
this . ticker = window . setInterval (() => this . _tick (), 1000 );
}
_tick () {
2026-07-26 14:57:18 +02:00
this . _tickBoosts ();
2026-06-22 16:41:53 +00:00
let flipped = false ;
this . root . querySelectorAll ( "[data-plot-timer]" ). forEach (( node ) => {
const readyAt = node . dataset . readyAt ;
const remaining = this . _remaining ( readyAt );
if ( remaining <= 0 ) {
node . textContent = "ready" ;
flipped = true ;
} else {
node . textContent = this . _format ( remaining );
}
});
if ( flipped ) this . load ();
}
_remaining ( iso ) {
if ( ! iso ) return 0 ;
const target = Date . parse ( iso );
if ( Number . isNaN ( target )) return 0 ;
return Math . max ( 0 , Math . floor (( target - Date . now ()) / 1000 ));
}
_format ( seconds ) {
2026-07-26 14:57:18 +02:00
return Format . duration ( seconds );
}
_setCoins ( selector , value ) {
const node = this . root . querySelector ( selector );
if ( ! node ) return ;
node . textContent = Format . number ( value );
node . title = Format . exact ( value );
}
_tickBoosts () {
this . root . querySelectorAll ( "[data-seconds]" ). forEach (( node ) => {
const remaining = Math . max ( 0 , Number ( node . dataset . seconds || 0 ) - 1 );
node . dataset . seconds = String ( remaining );
node . textContent = Format . duration ( remaining );
});
2026-06-22 16:41:53 +00:00
}
async _loadLeaderboard () {
const list = this . root . querySelector ( "[data-game-leaderboard]" );
if ( ! list ) return ;
try {
2026-07-21 03:36:29 +02:00
const data = await Http . getJson ( `/game/leaderboard?board= ${ encodeURIComponent ( this . board || "score" ) } ` );
2026-06-22 16:41:53 +00:00
if ( ! data . entries || ! data . entries . length ) {
list . innerHTML = `<li class="game-lb-empty">No farmers yet.</li>` ;
return ;
}
2026-07-21 03:36:29 +02:00
const board = this . board || "score" ;
2026-06-22 16:41:53 +00:00
list . innerHTML = data . entries
2026-07-21 03:36:29 +02:00
. map (( entry ) => {
const title = entry . title ? `<span class="game-lb-title"> ${ entry . title } </span>` : "" ;
const value = this . _leaderboardValue ( board , entry );
2026-07-25 11:58:02 +00:00
return `<li class="game-lb-row ${ entry . username === this . username ? " game-lb-self" : "" } "><span class="game-lb-rank"># ${ entry . rank } </span><div class="game-lb-name-group"><a class="game-lb-name" href="/game/farm/ ${ entry . username } "> ${ entry . username } </a> ${ title } </div><span class="game-lb-level">Lv ${ entry . level } </span><span class="game-lb-score"> ${ value } </span></li>` ;
2026-07-21 03:36:29 +02:00
})
2026-06-22 16:41:53 +00:00
. join ( "" );
} catch ( error ) {
return ;
}
}
2026-07-21 03:36:29 +02:00
_leaderboardValue ( board , entry ) {
2026-07-26 14:57:18 +02:00
if ( board === "raids" ) return ` ${ Format . coins ( entry . raid_avg ) } /raid` ;
if ( board === "time_to_kernel" ) return Format . duration ( entry . time_to_kernel_seconds );
return Format . exact ( entry . score );
2026-07-21 03:36:29 +02:00
}
2026-06-22 16:41:53 +00:00
}