197 lines
10 KiB
JavaScript
197 lines
10 KiB
JavaScript
"use strict";
|
|
|
|
function renderStats() {
|
|
const alive = world.tarinai;
|
|
const avg = (fn) => alive.length ? alive.reduce((a, t) => a + fn(t), 0) / alive.length : 0;
|
|
const environment = world.environmentScores ? world.environmentScores() : { security: 100, hygiene: 100, happiness: 100 };
|
|
const values = {
|
|
pop: alive.length,
|
|
dead: world.deadCount,
|
|
hunger: avg(t => t.hunger),
|
|
lonely: avg(t => t.loneliness),
|
|
stress: avg(t => t.stress),
|
|
lack: avg(t => t.lack),
|
|
security: environment.security,
|
|
hygiene: environment.hygiene,
|
|
happiness: environment.happiness,
|
|
objects: { ...(world.itemCounts || {}) },
|
|
};
|
|
setTextIfChanged(ui.statPop, "pop", values.pop);
|
|
setTextIfChanged(ui.statDead, "dead", values.dead);
|
|
updateColonyHistory(values);
|
|
drawColonyChart(values);
|
|
renderSelected();
|
|
if (world.familyTreeDirty) scheduleArchiveWindowRender();
|
|
}
|
|
|
|
function updateColonyHistory(values) {
|
|
const t = Math.max(0, world.time || 0);
|
|
const history = uiCache.chartHistory;
|
|
const interval = Math.max(1, (CONFIG.dayLength || 120) / 12);
|
|
const maxPoints = 7 * 12;
|
|
if ((history.length && t < history[history.length - 1].t) || (Number.isFinite(uiCache.chartBucketStart) && t < uiCache.chartBucketStart)) {
|
|
history.length = 0;
|
|
uiCache.chartBucketStart = -Infinity;
|
|
uiCache.chartBucketEnd = -Infinity;
|
|
uiCache.chartBucketAccum = null;
|
|
uiCache.environmentInitialRow = null;
|
|
uiCache.lastChartDraw = "";
|
|
}
|
|
const objectKeys = ["grass", "zunchi"];
|
|
const makeBucket = (start) => ({
|
|
start,
|
|
end: start + interval,
|
|
count: 0,
|
|
sums: { pop: 0, hunger: 0, lonely: 0, stress: 0, lack: 0, security: 0, hygiene: 0, happiness: 0 },
|
|
objects: Object.fromEntries(objectKeys.map(k => [k, 0])),
|
|
});
|
|
if (!uiCache.chartBucketAccum) {
|
|
const start = Math.floor(t / interval) * interval;
|
|
uiCache.chartBucketStart = start;
|
|
uiCache.chartBucketEnd = start + interval;
|
|
uiCache.chartBucketAccum = makeBucket(start);
|
|
}
|
|
const pushBucket = (bucket) => {
|
|
if (!bucket || bucket.count <= 0) return;
|
|
const row = {
|
|
t: bucket.end,
|
|
pop: bucket.sums.pop / bucket.count,
|
|
hunger: bucket.sums.hunger / bucket.count,
|
|
lonely: bucket.sums.lonely / bucket.count,
|
|
stress: bucket.sums.stress / bucket.count,
|
|
lack: bucket.sums.lack / bucket.count,
|
|
security: bucket.sums.security / bucket.count,
|
|
hygiene: bucket.sums.hygiene / bucket.count,
|
|
happiness: bucket.sums.happiness / bucket.count,
|
|
...Object.fromEntries(objectKeys.map(k => [`obj_${k}`, bucket.objects[k] / bucket.count])),
|
|
};
|
|
history.push(row);
|
|
if (history.length > maxPoints) history.splice(0, history.length - maxPoints);
|
|
};
|
|
while (t >= uiCache.chartBucketEnd) {
|
|
const b = uiCache.chartBucketAccum;
|
|
pushBucket(b);
|
|
uiCache.chartBucketStart = uiCache.chartBucketEnd;
|
|
uiCache.chartBucketEnd = uiCache.chartBucketStart + interval;
|
|
uiCache.chartBucketAccum = makeBucket(uiCache.chartBucketStart);
|
|
}
|
|
const bucket = uiCache.chartBucketAccum;
|
|
bucket.count += 1;
|
|
bucket.sums.pop += values.pop || 0;
|
|
bucket.sums.hunger += values.hunger || 0;
|
|
bucket.sums.lonely += values.lonely || 0;
|
|
bucket.sums.stress += values.stress || 0;
|
|
bucket.sums.lack += values.lack || 0;
|
|
bucket.sums.security += values.security || 0;
|
|
bucket.sums.hygiene += values.hygiene || 0;
|
|
bucket.sums.happiness += values.happiness || 0;
|
|
for (const k of objectKeys) bucket.objects[k] += (values.objects || {})[k] || 0;
|
|
}
|
|
|
|
function chartRows(values) {
|
|
const bucket = uiCache.chartBucketAccum;
|
|
const objectKeys = ["grass", "zunchi"];
|
|
const preview = bucket && bucket.count > 0 ? {
|
|
t: Math.max(world.time || 0, bucket.start || 0),
|
|
pop: bucket.sums.pop / bucket.count,
|
|
hunger: bucket.sums.hunger / bucket.count,
|
|
lonely: bucket.sums.lonely / bucket.count,
|
|
stress: bucket.sums.stress / bucket.count,
|
|
lack: bucket.sums.lack / bucket.count,
|
|
security: bucket.sums.security / bucket.count,
|
|
hygiene: bucket.sums.hygiene / bucket.count,
|
|
happiness: bucket.sums.happiness / bucket.count,
|
|
...Object.fromEntries(objectKeys.map(k => [`obj_${k}`, bucket.objects[k] / bucket.count])),
|
|
} : null;
|
|
return uiCache.chartHistory.length ? [...uiCache.chartHistory, ...(preview ? [preview] : [])] : [preview || { t: world.time || 0, ...values }];
|
|
}
|
|
|
|
function drawColonyChart(values) {
|
|
const chart = ui.colonyChart;
|
|
if (!chart) return;
|
|
const now = performance.now();
|
|
const perf = world.performanceLevel ? world.performanceLevel() : 0;
|
|
const minInterval = perf >= 3 ? 2600 : perf === 2 ? 1800 : perf === 1 ? 950 : 0;
|
|
if (minInterval && uiCache.lastChartDrawAt && now - uiCache.lastChartDrawAt < minInterval) return;
|
|
const mode = uiCache.chartMode || "population";
|
|
if (mode === "environment" && !uiCache.chartHistory.length && !uiCache.environmentInitialRow) {
|
|
uiCache.environmentInitialRow = { t: world.time || 0, ...values };
|
|
}
|
|
const rows = mode === "environment"
|
|
? (uiCache.chartHistory.length ? uiCache.chartHistory : [uiCache.environmentInitialRow || { t: world.time || 0, ...values }])
|
|
: chartRows(values);
|
|
const last = rows[rows.length - 1] || values;
|
|
const series = chartSeries(mode);
|
|
const seriesSnapshot = series.map(s => `${s.key}:${Math.round(Number(last[s.key] || 0) * 10)}`).join(",");
|
|
const snapshot = `${mode}:${rows.length}:${seriesSnapshot}:${chart.clientWidth}x${chart.clientHeight}`;
|
|
if (uiCache.lastChartDraw === snapshot) return;
|
|
uiCache.lastChartDraw = snapshot;
|
|
uiCache.lastChartDrawAt = now;
|
|
const cssW = Math.max(280, Math.floor(chart.clientWidth || chart.offsetWidth || 320));
|
|
const cssH = Math.max(160, Math.floor(chart.clientHeight || chart.offsetHeight || 188));
|
|
if (mode === "environment") return drawEnvironmentChart(chart, rows, cssW, cssH);
|
|
const pad = { l: 30, r: 12, t: 12, b: 24 };
|
|
const w = cssW - pad.l - pad.r;
|
|
const h = cssH - pad.t - pad.b;
|
|
const maxVal = Math.max(10, ...series.flatMap(s => rows.map(r => Number(r[s.key] || 0))));
|
|
const xFor = (i) => rows.length <= 1 ? pad.l + w : pad.l + (i / (rows.length - 1)) * w;
|
|
const yFor = (row, s) => {
|
|
const raw = Number(row[s.key] || 0);
|
|
const scale = s.percent ? 100 : maxVal;
|
|
const v = clamp(raw / Math.max(scale, 1) * 100, 0, 100);
|
|
return pad.t + h - h * v / 100;
|
|
};
|
|
const grid = [0, 25, 50, 75, 100].map(yv => {
|
|
const y = pad.t + h - h * yv / 100;
|
|
return `<line x1="${pad.l}" y1="${y.toFixed(1)}" x2="${(pad.l + w).toFixed(1)}" y2="${y.toFixed(1)}" stroke="rgba(84,68,48,0.13)" stroke-width="1"/><text x="${pad.l - 6}" y="${(y + 3).toFixed(1)}" text-anchor="end" font-size="10" fill="rgba(117,106,94,0.78)">${yv}</text>`;
|
|
}).join("");
|
|
const lines = series.map(s => {
|
|
const pts = rows.map((row, i) => `${xFor(i).toFixed(1)},${yFor(row, s).toFixed(1)}`).join(" ");
|
|
const last = rows[rows.length - 1];
|
|
const cx = xFor(rows.length - 1).toFixed(1);
|
|
const cy = yFor(last, s).toFixed(1);
|
|
return `<polyline points="${pts}" fill="none" stroke="${s.color}" stroke-width="${s.key === "pop" ? 2.4 : 1.8}" stroke-linecap="round" stroke-linejoin="round"/><circle cx="${cx}" cy="${cy}" r="2.7" fill="${s.color}"/>`;
|
|
}).join("");
|
|
chart.innerHTML = `<svg viewBox="0 0 ${cssW} ${cssH}" preserveAspectRatio="none" aria-hidden="true"><rect x="0" y="0" width="${cssW}" height="${cssH}" rx="14" fill="rgba(255,255,255,0.18)"/>${grid}${lines}<text x="${pad.l}" y="${cssH - 8}" font-size="10" fill="rgba(117,106,94,0.76)">2\u6642\u9593\u3054\u3068 / 1\u9031\u9593</text></svg>`;
|
|
if (ui.colonyChartLegend) ui.colonyChartLegend.innerHTML = series.map(s => `<span><i style="background:${s.color}"></i>${s.label} ${formatChartValue(s.key, rows[rows.length - 1] || values)}</span>`).join("");
|
|
}
|
|
|
|
function drawEnvironmentChart(chart, rows, cssW, cssH) {
|
|
const series = chartSeries("environment");
|
|
const row = rows[rows.length - 1] || {};
|
|
const pad = { l: 58, r: 18, t: 16, b: 18 };
|
|
const barW = cssW - pad.l - pad.r;
|
|
const rowH = Math.max(30, (cssH - pad.t - pad.b) / series.length);
|
|
const bars = series.map((s, i) => {
|
|
const y = pad.t + i * rowH + 6;
|
|
const v = clamp(Number(row[s.key] || 0), 0, 100);
|
|
const bw = barW * v / 100;
|
|
return `<text x="${pad.l - 8}" y="${(y + 13).toFixed(1)}" text-anchor="end" font-size="11" fill="rgba(84,68,48,0.80)">${s.label}</text><rect x="${pad.l}" y="${y.toFixed(1)}" width="${barW}" height="${Math.max(12, rowH - 14).toFixed(1)}" rx="7" fill="rgba(84,68,48,0.08)"/><rect x="${pad.l}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(12, rowH - 14).toFixed(1)}" rx="7" fill="${s.color}"/><text x="${(pad.l + Math.min(barW - 26, Math.max(6, bw + 7))).toFixed(1)}" y="${(y + 13).toFixed(1)}" font-size="11" fill="rgba(42,36,29,0.82)">${Math.round(v)}</text>`;
|
|
}).join("");
|
|
chart.innerHTML = `<svg viewBox="0 0 ${cssW} ${cssH}" preserveAspectRatio="none" aria-hidden="true"><rect x="0" y="0" width="${cssW}" height="${cssH}" rx="14" fill="rgba(255,255,255,0.18)"/>${bars}<text x="${pad.l}" y="${cssH - 5}" font-size="10" fill="rgba(117,106,94,0.76)">0-100</text></svg>`;
|
|
if (ui.colonyChartLegend) ui.colonyChartLegend.innerHTML = series.map(s => `<span><i style="background:${s.color}"></i>${s.label} ${formatChartValue(s.key, row)}</span>`).join("");
|
|
}
|
|
|
|
function chartSeries(mode = "population") {
|
|
if (mode === "life") return [
|
|
{ key: "hunger", label: "\u7a7a\u8179", color: "#e4a33f", percent: true },
|
|
{ key: "lonely", label: "\u5bc2\u3057\u3055", color: "#9b78d4", percent: true },
|
|
{ key: "stress", label: "\u30b9\u30c8\u30ec\u30b9", color: "#d96262", percent: true },
|
|
{ key: "lack", label: "\u305f\u308a\u306a\u3044", color: "#5b9d61", percent: true },
|
|
];
|
|
if (mode === "objects") return [
|
|
{ key: "obj_grass", label: "\u8349", color: "#5b9d61" },
|
|
{ key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f8a3f" },
|
|
];
|
|
if (mode === "environment") return [
|
|
{ key: "security", label: "\u6cbb\u5b89", color: "#d65e46", percent: true },
|
|
{ key: "hygiene", label: "\u885b\u751f", color: "#6f8a3f", percent: true },
|
|
{ key: "happiness", label: "\u5e78\u798f", color: "#d98bc0", percent: true },
|
|
];
|
|
return [{ key: "pop", label: "\u500b\u4f53", color: "#5d8ee6" }];
|
|
}
|
|
|
|
function formatChartValue(key, values) {
|
|
const v = values[key] ?? 0;
|
|
return String(Math.round(v));
|
|
}
|