// retoor import { Format } from "./Format.js"; 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; this.board = "score"; this._bindActions(); this._bindLeaderboardBoard(); 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); if (window.app && window.app.dialog && window.app.dialog.isOpen) return; 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); }); } _bindLeaderboardBoard() { const select = this.root.querySelector("[data-lb-board]"); if (!select) return; select.addEventListener("change", () => { this.board = select.value; this._loadLeaderboard(); }); } 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); if (action === "steal" && window.app && window.app.toast) { window.app.toast.show(`You stole ${Format.coins(data.stole_coins)}!`, { type: "success" }); } if (action === "harvest" || action === "water" || action === "steal" || action === "prestige") { 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)); this._setHost("[data-defense-host]", this._defenseHtml(farm)); this._setHost("[data-infra-host]", this._infraHtml(farm)); this._setHost("[data-perk-host]", this._perksHtml(farm)); this._setHost("[data-legacy-host]", this._legacyHtml(farm)); this._setHost("[data-mastery-host]", this._masteryHtml(farm)); this._setHost("[data-analytics-host]", this._analyticsHtml(farm)); this._setHost("[data-cosmetics-host]", this._cosmeticsHtml(farm)); this._setHost("[data-daily-host]", this._dailyHtml(farm)); this._setHost("[data-grant-host]", this._grantHtml(farm)); 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; }; this._setCoins("[data-hud-coins]", farm.coins); set("[data-hud-level]", farm.level); set("[data-hud-ci]", farm.ci_tier); set("[data-hud-ci-label]", `${farm.ci_label} (${Format.speed(farm.ci_speed)})`); set("[data-hud-harvests]", Format.exact(farm.total_harvests)); set("[data-hud-prestige]", farm.prestige); set("[data-hud-prestige-mult]", `+${Math.round(farm.prestige_multiplier * 100 - 100)}% coins`); 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)); 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); fill.style.setProperty("--hud-xp", `${pct}%`); } set("[data-hud-xp]", farm.level_is_max ? "max level" : `${Format.exact(farm.level_into)} / ${Format.exact(farm.level_span)} XP`); } _shopHtml(farm) { const ciUpgrade = farm.ci_next_tier ? `
` : ""; const ciText = farm.ci_next_tier ? `Upgrade to ${farm.ci_next_label} for faster builds` : "CI is at the top tier."; const prestigeBtn = farm.refactor_affordable ? `
` : ""; let prestigeText; if (!farm.prestige_available) { prestigeText = `Reach level ${farm.prestige_min_level} to refactor.`; } else if (farm.refactor_affordable) { 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.`; } else { prestigeText = `Refactoring costs ${Format.coins(farm.refactor_cost)} right now - keep farming to afford it.`; } return ( `
CI upgrade${ciText}
${ciUpgrade}
` + `
Refactor (prestige ${farm.prestige})${prestigeText}
${prestigeBtn}
` ); } _perksHtml(farm) { return (farm.perks || []) .map((perk) => { const action = perk.maxed ? `Maxed` : `
`; return `
${perk.name}Lv ${perk.level}/${perk.max_level}${perk.effect || perk.description}${action}
`; }) .join(""); } _legacyHtml(farm) { return (farm.legacy || []) .map((up) => { const action = up.maxed ? `Maxed` : `
`; return `
${up.name}Lv ${up.level}/${up.max_level}${up.effect || up.description}${action}
`; }) .join(""); } _masteryHtml(farm) { return (farm.mastery || []) .map((m) => { const action = m.maxed ? `Maxed` : `
`; return `
${m.name}Lv ${m.level}/${m.max_level}${m.effect || m.description}${action}
`; }) .join(""); } _analyticsHtml(farm) { if (!farm.mastery_analytics_unlocked) return ""; return ( `
Lifetime coins earned${Format.coins(farm.lifetime_coins_earned)} since Farm Analytics was unlocked.
` + `
Lifetime harvests${Format.exact(farm.lifetime_harvests)} builds harvested since Farm Analytics was unlocked.
` ); } _infraHtml(farm) { return (farm.infrastructure || []) .map((infra) => { let action; if (infra.owned) { action = `Owned`; } else if (farm.prestige < infra.min_prestige) { action = `Requires prestige ${infra.min_prestige}`; } else { action = `
`; } return `
${infra.name}${infra.description}${action}
`; }) .join(""); } _defenseHtml(farm) { const upkeep = farm.defense_upkeep_daily ? `Costs ${Format.coins(farm.defense_upkeep_daily)}/day upkeep. If you cannot pay you lose one level - never your whole balance.` : "No upkeep at level 0."; const action = farm.defense_next_cost ? `
` : `Maxed`; const downgrade = farm.defense_downgrade_available ? `
` : ""; return `
Defense: ${farm.defense_tier_name} (level ${farm.defense_level})${upkeep}
${action}${downgrade}
`; } _cosmeticsHtml(farm) { return (farm.cosmetics || []) .map((c) => { let action; if (c.owned && c.kind === "title") { action = farm.active_title === c.key ? `Equipped` : `
`; } else if (c.owned) { action = `Owned`; } else { action = `
`; } return `
${c.name}${c.description}${action}
`; }) .join(""); } _grantHtml(farm) { const action = farm.grant_available ? `
` : `${farm.grant_reason}`; return `

Treasury: ${Format.coins(farm.treasury_balance)} collected from refactor fees

${action}`; } _dailyHtml(farm) { const action = farm.daily_available ? `
` : `Claimed today. Come back tomorrow.`; return `

Streak: ${Format.exact(farm.streak)} day(s)${farm.daily_streak_reset ? " - your streak lapsed, claiming today starts a new one" : ""}

${action}`; } _questsHtml(farm) { const quests = farm.quests || []; if (!quests.length) return `
  • No quests today.
  • `; return quests .map((quest) => { const pct = quest.goal ? Math.floor((100 * quest.progress) / quest.goal) : 0; const reward = quest.reward_stars ? `+${Format.exact(quest.reward_stars)} ★` : `+${Format.coins(quest.reward_coins)}`; let foot = ""; if (quest.can_claim) { foot = `
    `; } else if (quest.claimed) { foot = `Done`; } return `
  • ${quest.label}${reward}
    ${Format.exact(quest.progress)}/${Format.exact(quest.goal)}${foot}
  • `; }) .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) { buy = `
    `; } return `
    ${cells}${buy}
    `; } _plotHtml(plot, farm) { const golden = plot.is_golden ? " game-plot-golden" : ""; const head = `
    `; 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})` : ""; const market = crop.market_state === "saturated" ? " (saturated)" : crop.market_state === "boosted" ? " (boosted)" : ""; return ``; }) .join(""); body = `
    `; } else { body = `Empty plot`; } } else if (plot.state === "growing") { body = `${plot.crop_name}building${plot.watered_count}/${plot.max_waters} 💧`; if (plot.can_water) { body += `
    `; } if (farm.is_owner && plot.fertilize_cost) { body += `
    `; } } else { const goldenMark = plot.is_golden ? " ✨" : ""; const raided = plot.raided_pct ? `Raided ${plot.raided_pct}%` : ""; body = `${plot.crop_name}${goldenMark}${raided}`; if (farm.is_owner) { const label = plot.is_golden ? "Harvest golden" : "Harvest"; body += `
    `; } else if (plot.can_steal) { body += `
    `; } else if (plot.steal_reason === "stripped") { body += `Already stripped`; } else if (plot.steal_reason === "cooldown") { body += `Raid again in ${this._format(plot.steal_cooldown_seconds)}`; } else { body += `Ready`; } } return `${head}${body}
    `; } _startTicker() { this.ticker = window.setInterval(() => this._tick(), 1000); } _tick() { this._tickBoosts(); 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) { 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); }); } async _loadLeaderboard() { const list = this.root.querySelector("[data-game-leaderboard]"); if (!list) return; try { const data = await Http.getJson(`/game/leaderboard?board=${encodeURIComponent(this.board || "score")}`); if (!data.entries || !data.entries.length) { list.innerHTML = `
  • No farmers yet.
  • `; return; } const board = this.board || "score"; list.innerHTML = data.entries .map((entry) => { const title = entry.title ? `${entry.title}` : ""; const value = this._leaderboardValue(board, entry); return `
  • #${entry.rank}Lv ${entry.level}${value}
  • `; }) .join(""); } catch (error) { return; } } _leaderboardValue(board, entry) { 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); } }