"use strict"; function colonyMoodTip() { const mood = world?.evaluateColonyMood?.() || world?.colonyMood || null; return String(mood?.description || "\u5927\u304d\u306a\u554f\u984c\u304c\u5c11\u306a\u304f\u3001\u843d\u3061\u7740\u3044\u305f\u72b6\u614b\u3067\u3059\u3002"); } function bindColonyMoodTooltip() { const el = ui?.statColonyMood; const target = el?.closest?.(".stat") || el; if (!target || target.dataset.colonyMoodTipBound === "1") return; target.dataset.colonyMoodTipBound = "1"; target.tabIndex = target.tabIndex >= 0 ? target.tabIndex : 0; window.TarinaiTooltips.bind(target, colonyMoodTip); } function colonyChartObjectKeys() { return ["grass", "zunchi", "trace", "grave", "grass_bed", "water"]; } function colonyStatsUiVisible() { if (typeof document !== "undefined" && document.hidden) return false; const card = ui?.colonyChart?.closest?.(".colony-card") || null; if (!card) return Boolean(ui?.colonyChart); if (card.classList?.contains?.("hidden") || card.classList?.contains?.("collapsed")) return false; return true; } function collectDynamicColonyStats(options = {}) { const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now(); const cache = world._colonyStatsDynamicCache; const cacheAge = now - (Number(world._colonyStatsDynamicCacheAt) || 0); // Colony cards can ask for a refresh from many UI paths. Reuse one aggregate // for a short real-time window instead of rescanning every Tarinai each time. // Fixed history samples opt into force=true so their recorded values remain // tied to the requested simulation timestamp. if (options.force !== true && cache && cacheAge >= 0 && cacheAge < 800) return cache; const needKeys = typeof TARINAI_NEED_KEYS !== "undefined" ? TARINAI_NEED_KEYS : ["food", "sleep", "health", "safety", "social", "fulfill"]; const needSums = Object.fromEntries(needKeys.map(key => [key, 0])); let liveCount = 0; let sickCount = 0; let juvenileCount = 0; let elderCount = 0; let stressSum = 0; for (const t of world.tarinai || []) { if (!t || t.dead) continue; liveCount += 1; if (t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease) sickCount += 1; const ratio = Math.max(0, Math.min(1, (Number(t.age || 0) || 0) / Math.max(1, Number(t.lifeSpan || 1) || 1))); if (ratio <= 0.10) juvenileCount += 1; if (ratio >= 0.80) elderCount += 1; stressSum += Number(t.stress || 0) || 0; for (const key of needKeys) needSums[key] += Number(t.needs?.[key] || 0) || 0; } const divisor = Math.max(1, liveCount); const dynamic = { sick: sickCount, juvenile: juvenileCount, elder: elderCount, stress: liveCount ? stressSum / divisor : 0, ants: (world.ants || []).reduce((n, ant) => n + (ant && !ant.dead ? 1 : 0), 0), ...Object.fromEntries(needKeys.map(key => [`need_${key}`, liveCount ? needSums[key] / divisor : 0])), }; world._colonyStatsDynamicCache = dynamic; world._colonyStatsDynamicCacheAt = now; return dynamic; } function renderStats(options = {}) { const colonyVisible = options.force === true || colonyStatsUiVisible(); // Selected-data and family views are separate surfaces. Keep them fresh // without paying for colony aggregation while the colony card is hidden. if (!colonyVisible) { if (ui?.selectedCard && !ui.selectedCard.classList.contains("hidden")) renderSelected(); return; } const counts = world.tarinaiCounts?.() || world.rebuildTarinaiCountCache?.("stats") || { alive: 0, zunchiSlaves: 0, tarinaiKings: 0 }; const liveCount = Math.max(0, Number(counts.alive || 0) || 0); const temp = world.currentTemperature ?? world.updateTemperature?.(0) ?? (CONFIG.standardTemperature ?? 15); const dynamic = collectDynamicColonyStats(); const values = { pop: liveCount, zunchiSlaves: Math.max(0, Number(counts.zunchiSlaves || 0) || 0), tarinaiKings: Math.max(0, Number(counts.tarinaiKings || 0) || 0), sick: dynamic.sick || 0, juvenile: dynamic.juvenile || 0, elder: dynamic.elder || 0, dead: world.deadCount, birthEvents: Math.max(0, Number(world.birthEventCount || 0) || 0), deathEvents: Math.max(0, Number(world.deadCount || 0) || 0), stress: dynamic.stress || 0, temperature: Number(temp) || 0, ants: dynamic.ants || 0, graves: (() => { const bucket = typeof world.itemsOfType === "function" ? world.itemsOfType("grave") : null; if (bucket && typeof bucket.length === "number") return Math.max(0, bucket.length); let count = 0; for (const item of world.items || []) if (item && !item.dead && item.type === "grave") count += 1; return count; })(), ...dynamic, objects: world.itemCounts || {}, }; setTextIfChanged(ui.statSick, "sick", values.sick); setTextIfChanged(ui.statJuvenile, "juvenile", values.juvenile); setTextIfChanged(ui.statElder, "elder", values.elder); const colonyMood = world.evaluateColonyMood?.() || world.colonyMood || { label: "\u5b89\u5b9a" }; setTextIfChanged(ui.statColonyMood, "colonyMood", colonyMood?.label || "\u5b89\u5b9a"); const tempLabel = `${Math.round(temp)}\u2103`; setTextIfChanged(ui.temperatureValue, "temperature", tempLabel); if (ui.temperatureSlider) { ui.temperatureSlider.disabled = world.temperatureAuto !== false; if (document.activeElement !== ui.temperatureSlider || world.temperatureAuto !== false) ui.temperatureSlider.value = String(Math.round(temp)); ui.temperatureSlider.setAttribute("aria-valuetext", tempLabel); } if (ui.temperatureAutoToggle) ui.temperatureAutoToggle.checked = world.temperatureAuto !== false; const tarinaiLimit = Math.max(0, Number(world.tarinaiPopulationLimit) || 0); const objectLimit = Math.max(0, Number(world.objectLimit) || 0); const objectCount = world.activeObjectCount?.() ?? 0; if (typeof window.syncColonyLimitControls === "function") { window.syncColonyLimitControls("tarinai", tarinaiLimit); window.syncColonyLimitControls("object", objectLimit); } else { if (ui.tarinaiPopulationLimitInput && document.activeElement !== ui.tarinaiPopulationLimitInput) ui.tarinaiPopulationLimitInput.value = tarinaiLimit > 0 ? String(tarinaiLimit) : ""; if (ui.objectLimitInput && document.activeElement !== ui.objectLimitInput) ui.objectLimitInput.value = objectLimit > 0 ? String(objectLimit) : ""; } setTextIfChanged(ui.colonyLimitStatus, "colonyLimitStatus", `\u305f\u308a\u306a\u3044\u306e\u6570 ${values.pop} / ${tarinaiLimit || "\u221e"}\u3000\u7269\u4f53\u6570 ${objectCount} / ${objectLimit || "\u221e"}`); bindColonyMoodTooltip(); window.TarinaiTooltips.refresh(ui.statColonyMood?.closest?.(".stat") || ui.statColonyMood); window.TarinaiGroundUI.update(world); drawColonyChart(values); if (ui?.selectedCard && !ui.selectedCard.classList.contains("hidden")) renderSelected(); if (world.familyTreeDirty) scheduleArchiveWindowRender(); } function updateColonyHistory(values, options = {}) { const t = Math.max(0, Number(world.time || 0) || 0); const history = uiCache.chartHistory; const interval = Math.max(1, (CONFIG.dayLength || 120) / 12); const maxPoints = 20 * 12; const birthsTotal = Math.max(0, Number(values.birthEvents || 0) || 0); const deathsTotal = Math.max(0, Number(values.deathEvents || 0) || 0); const lastT = history.length ? Number(history[history.length - 1]?.t || 0) : -Infinity; const rewound = t + 1e-6 < lastT || (Number.isFinite(uiCache.chartNextSampleAt) && t + interval < uiCache.chartNextSampleAt) || (Number.isFinite(uiCache.chartBirthEventSeen) && birthsTotal < uiCache.chartBirthEventSeen) || (Number.isFinite(uiCache.chartDeathEventSeen) && deathsTotal < uiCache.chartDeathEventSeen); if (rewound) { history.length = 0; uiCache.chartNextSampleAt = 0; uiCache.chartBirthEventSeen = null; uiCache.chartDeathEventSeen = null; uiCache.lastChartDraw = ""; } if (!Number.isFinite(uiCache.chartNextSampleAt)) uiCache.chartNextSampleAt = 0; if (options.force !== true && t + 1e-6 < uiCache.chartNextSampleAt) return false; if (!Number.isFinite(uiCache.chartBirthEventSeen)) uiCache.chartBirthEventSeen = birthsTotal; if (!Number.isFinite(uiCache.chartDeathEventSeen)) uiCache.chartDeathEventSeen = deathsTotal; const births = Math.max(0, birthsTotal - uiCache.chartBirthEventSeen); const deaths = Math.max(0, deathsTotal - uiCache.chartDeathEventSeen); uiCache.chartBirthEventSeen = birthsTotal; uiCache.chartDeathEventSeen = deathsTotal; const objectKeys = colonyChartObjectKeys(); const needKeys = typeof TARINAI_NEED_KEYS !== "undefined" ? TARINAI_NEED_KEYS : ["food", "sleep", "health", "safety", "social", "fulfill"]; const sampleT = Math.max(0, Math.floor(t / interval) * interval); const row = { t: sampleT, pop: values.pop || 0, zunchiSlaves: values.zunchiSlaves || 0, tarinaiKings: values.tarinaiKings || 0, sick: values.sick || 0, juvenile: values.juvenile || 0, elder: values.elder || 0, ants: values.ants || 0, stress: values.stress || 0, temperature: Number(values.temperature || 0) || 0, ...Object.fromEntries(needKeys.map(k => [`need_${k}`, values[`need_${k}`] || 0])), ...Object.fromEntries(objectKeys.map(k => [`obj_${k}`, k === "grave" ? (values.graves || 0) : ((values.objects || {})[k] || 0)])), births, deaths, }; if (history.length && Math.abs((Number(history[history.length - 1]?.t) || 0) - sampleT) < 1e-6) history[history.length - 1] = row; else history.push(row); if (history.length > maxPoints) history.splice(0, history.length - maxPoints); uiCache.chartNextSampleAt = sampleT + interval; return true; } function collectColonyHistorySampleValues() { const counts = world.tarinaiCounts?.() || world.rebuildTarinaiCountCache?.("chart-sample") || { alive: 0, zunchiSlaves: 0, tarinaiKings: 0 }; const dynamic = collectDynamicColonyStats({ force: true }); const graveBucket = typeof world.itemsOfType === "function" ? world.itemsOfType("grave") : null; let graves = 0; if (graveBucket && typeof graveBucket.length === "number") graves = Math.max(0, graveBucket.length); else for (const item of world.items || []) if (item && !item.dead && item.type === "grave") graves += 1; return { pop: Math.max(0, Number(counts.alive || 0) || 0), zunchiSlaves: Math.max(0, Number(counts.zunchiSlaves || 0) || 0), tarinaiKings: Math.max(0, Number(counts.tarinaiKings || 0) || 0), sick: dynamic.sick || 0, juvenile: dynamic.juvenile || 0, elder: dynamic.elder || 0, ants: dynamic.ants || 0, stress: dynamic.stress || 0, temperature: Number(world.currentTemperature ?? world.updateTemperature?.(0) ?? (CONFIG.standardTemperature ?? 15)) || 0, birthEvents: Math.max(0, Number(world.birthEventCount || 0) || 0), deathEvents: Math.max(0, Number(world.deadCount || 0) || 0), graves, ...dynamic, objects: world.itemCounts || {}, }; } function tickColonyHistorySampling() { const t = Math.max(0, Number(world?.time || 0) || 0); const interval = Math.max(1, (CONFIG.dayLength || 120) / 12); if (!Number.isFinite(uiCache.chartNextSampleAt)) uiCache.chartNextSampleAt = 0; if (t + 1e-6 < uiCache.chartNextSampleAt) return false; return updateColonyHistory(collectColonyHistorySampleValues(), { force: true }); } function chartRangeConfig() { const dayLength = Math.max(1, Number(CONFIG.dayLength || 120) || 120); const range = ["day", "season", "year"].includes(uiCache.chartRange) ? uiCache.chartRange : "season"; if (range === "day") return { id: range, duration: dayLength, footer: "\u0032\u6642\u9593\u3054\u3068 / 1\u65e5" }; if (range === "year") return { id: range, duration: dayLength * 20, footer: "1\u65e51\u70b9 / 20\u65e5\u9593" }; return { id: "season", duration: dayLength * 5, footer: "\u0032\u6642\u9593\u3054\u3068 / 5\u65e5\u9593" }; } function yearRepresentativeRows(rows, minT, maxT) { const dayLength = Math.max(1, Number(CONFIG.dayLength || 120) || 120); const grouped = new Map(); for (const row of rows || []) { const t = Math.max(0, Number(row?.t || 0) || 0); if (t < minT - 1e-6 || t > maxT + 1e-6) continue; const day = Math.floor(t / dayLength); const noon = day * dayLength + dayLength * 0.5; const prev = grouped.get(day); if (!prev || Math.abs(t - noon) < Math.abs((Number(prev.t || 0) || 0) - noon)) grouped.set(day, row); } return Array.from(grouped.entries()).sort((a, b) => a[0] - b[0]).map(([, row]) => row); } function chartRows(values) { const allRows = uiCache.chartHistory.length ? uiCache.chartHistory.slice() : []; const now = Math.max(0, Number(world.time || 0) || 0); const interval = Math.max(1, (CONFIG.dayLength || 120) / 12); // Keep the visible time window anchored to the newest recorded sample rather // than the continuously advancing world clock. The graph now shifts left at // exactly the same moment a new data point is appended. const maxT = allRows.length ? Math.max(0, Number(allRows[allRows.length - 1]?.t || 0) || 0) : Math.max(0, Math.floor(now / interval) * interval); const config = chartRangeConfig(); const minT = Math.max(0, maxT - config.duration); let rows = allRows.filter(row => { const t = Math.max(0, Number(row?.t || 0) || 0); return t >= minT - 1e-6 && t <= maxT + 1e-6; }); if (config.id === "year") rows = yearRepresentativeRows(rows, minT, maxT); if (rows.length) return rows; const objectKeys = colonyChartObjectKeys(); const needKeys = typeof TARINAI_NEED_KEYS !== "undefined" ? TARINAI_NEED_KEYS : ["food", "sleep", "health", "safety", "social", "fulfill"]; return [{ t: now, pop: 0, zunchiSlaves: 0, tarinaiKings: 0, sick: 0, juvenile: 0, elder: 0, ants: 0, stress: 0, temperature: 0, births: 0, deaths: 0, ...Object.fromEntries(needKeys.map(k => [`need_${k}`, 0])), ...Object.fromEntries(objectKeys.map(k => [`obj_${k}`, 0])), }]; } function chartSeasonDayLabelForTime(t = 0) { const dayLength = Math.max(1, Number(CONFIG.dayLength || 120) || 120); const dayNumber = Math.floor(Math.max(0, Number(t || 0) || 0) / dayLength) + 1; const seasonIndex = Math.floor(((dayNumber - 1) % 20) / 5); const season = ["\u6625", "\u590f", "\u79cb", "\u51ac"][seasonIndex] || "\u6625"; const seasonDay = ((dayNumber - 1) % 5) + 1; return `${season}${seasonDay}\u65e5`; } function chartDateTimeLabelForTime(t = 0) { const dayLength = Math.max(1, Number(CONFIG.dayLength || 120) || 120); const safe = Math.max(0, Number(t || 0) || 0); const dayStart = Math.floor(safe / dayLength) * dayLength; const dayRatio = Math.max(0, Math.min(0.999999, (safe - dayStart) / dayLength)); const totalMinutes = Math.floor(dayRatio * 24 * 60); const hh = String(Math.floor(totalMinutes / 60)).padStart(2, "0"); const mm = String(totalMinutes % 60).padStart(2, "0"); return `${chartSeasonDayLabelForTime(safe)} ${hh}:${mm}`; } function bindColonyChartHover() { const chart = ui?.colonyChart; if (!chart || chart.dataset.historyHoverBound === "1") return; chart.dataset.historyHoverBound = "1"; let tooltip = document.getElementById("colonyChartFloatingTooltip"); if (!tooltip) { tooltip = document.createElement("div"); tooltip.id = "colonyChartFloatingTooltip"; tooltip.className = "colony-chart-tooltip hidden"; document.body.appendChild(tooltip); } const hoverMarker = () => { let marker = chart.querySelector(".colony-chart-hover-point"); if (!marker) { marker = document.createElement("div"); marker.className = "colony-chart-hover-point hidden"; chart.appendChild(marker); } return marker; }; const hide = () => { tooltip.classList.add("hidden"); hoverMarker().classList.add("hidden"); }; chart.addEventListener("mouseleave", hide); chart.addEventListener("mousemove", (event) => { const state = uiCache.chartHoverState; if (!state?.rows?.length) return hide(); const rect = chart.getBoundingClientRect(); if (!rect.width || !rect.height) return hide(); const px = Math.max(0, Math.min(state.cssW, (event.clientX - rect.left) * state.cssW / rect.width)); const py = Math.max(0, Math.min(state.cssH, (event.clientY - rect.top) * state.cssH / rect.height)); const plotX = Math.max(state.pad.l, Math.min(state.pad.l + state.w, px)); const ratio = (plotX - state.pad.l) / Math.max(1, state.w); const targetT = state.minT + ratio * Math.max(1, state.maxT - state.minT); let nearest = state.rows[0]; let best = Infinity; for (const row of state.rows) { const d = Math.abs((Number(row?.t || 0) || 0) - targetT); if (d < best) { best = d; nearest = row; } } if (!nearest) return hide(); const snappedX = state.pad.l + clamp(((Number(nearest.t || 0) || 0) - state.minT) / Math.max(1, state.maxT - state.minT), 0, 1) * state.w; const scale = state.scale || { min: 0, max: 100, percent: false }; const yForSeries = (seriesDef) => { const raw = Number(nearest?.[seriesDef.key] || 0); if (scale.percent) return state.pad.t + state.h - state.h * clamp(raw / 100, 0, 1); const denom = Math.max(1, scale.max - scale.min); return state.pad.t + state.h - state.h * clamp((raw - scale.min) / denom, 0, 1); }; let snapSeries = state.series[0] || null; let snappedY = snapSeries ? yForSeries(snapSeries) : state.pad.t + state.h * 0.5; let bestY = Math.abs(snappedY - py); for (const seriesDef of state.series.slice(1)) { const candidateY = yForSeries(seriesDef); const distance = Math.abs(candidateY - py); if (distance < bestY) { bestY = distance; snapSeries = seriesDef; snappedY = candidateY; } } const marker = hoverMarker(); marker.style.left = `${snappedX * rect.width / state.cssW}px`; marker.style.top = `${snappedY * rect.height / state.cssH}px`; if (snapSeries?.color) marker.style.borderColor = snapSeries.color; marker.classList.remove("hidden"); const lines = state.series.map(series => `