tarinai/js/ui_charts.js
2026-06-20 15:55:52 +09:00

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));
}