ooo
This commit is contained in:
parent
e2d2dfcb66
commit
8f80997b0d
256 changed files with 4044 additions and 966 deletions
142
boot.js
142
boot.js
|
|
@ -7,6 +7,14 @@ updateStats(forceStats);
|
|||
renderTooltip();
|
||||
renderDirty = false;
|
||||
lastRenderAt = performance.now();
|
||||
if (!fpsWindowStartedAt) fpsWindowStartedAt = lastRenderAt;
|
||||
fpsFrames++;
|
||||
const fpsElapsed = lastRenderAt - fpsWindowStartedAt;
|
||||
if (fpsElapsed >= 500) {
|
||||
if (els.fpsCounter) els.fpsCounter.textContent = `FPS ${Math.round(fpsFrames * 1000 / fpsElapsed)}`;
|
||||
fpsWindowStartedAt = lastRenderAt;
|
||||
fpsFrames = 0;
|
||||
}
|
||||
}
|
||||
function scheduleLoop() {
|
||||
if (loopScheduled) return;
|
||||
|
|
@ -28,7 +36,7 @@ loopScheduled = false;
|
|||
if (running && !document.hidden && now - lastSimTickAt >= LoopConfig.simTickMs) {
|
||||
lastSimTickAt = now;
|
||||
const steps = Number(els.speed.value);
|
||||
const stepBudget = LoopConfig.stepBudgetMs * Math.max(1, steps);
|
||||
const stepBudget = LoopConfig.stepBudgetMs;
|
||||
const loopStart = performance.now();
|
||||
let completedSteps = 0;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
|
|
@ -38,30 +46,126 @@ if (completedSteps > 0 && performance.now() - loopStart > stepBudget) break;
|
|||
}
|
||||
if (completedSteps > 0) requestRender();
|
||||
}
|
||||
const shouldRender = !document.hidden &&
|
||||
(renderDirty || (running && now - lastRenderAt >= LoopConfig.renderMs));
|
||||
const shouldRender = !document.hidden && renderDirty &&
|
||||
(!running || now - lastRenderAt >= LoopConfig.renderMs);
|
||||
if (shouldRender) drawFrame();
|
||||
scheduleLoop();
|
||||
}
|
||||
function reset() {
|
||||
const size = Number(els.worldSize.value);
|
||||
const initialPopulation = Number(els.agentCount.value);
|
||||
|
||||
let interventionMode = null;
|
||||
let manualRouteStartCityId = null;
|
||||
|
||||
function setInterventionStatus(text) {
|
||||
if (els.interventionStatus) els.interventionStatus.textContent = text;
|
||||
}
|
||||
function setInterventionMode(mode) {
|
||||
interventionMode = interventionMode === mode ? null : mode;
|
||||
manualRouteStartCityId = null;
|
||||
const buttons = [
|
||||
[els.triggerDisaster, "disaster"],
|
||||
[els.buildCity, "city"],
|
||||
[els.connectCities, "route"]
|
||||
];
|
||||
for (const [button, buttonMode] of buttons) button?.setAttribute("aria-pressed", String(interventionMode === buttonMode));
|
||||
if (interventionMode === "disaster") setInterventionStatus("災害を発生させる地点を地図上でクリックしてください。");
|
||||
else if (interventionMode === "city") setInterventionStatus("都市を建設する地点を地図上でクリックしてください。");
|
||||
else if (interventionMode === "route") setInterventionStatus("接続する1つ目の都市をクリックしてください。");
|
||||
else setInterventionStatus("ボタンを選び、地図上をクリックして実行します。");
|
||||
}
|
||||
function eventToWorldCoordinates(event) {
|
||||
const rect = els.canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.floor((event.clientX - rect.left) / rect.width * sim.world.size),
|
||||
y: Math.floor((event.clientY - rect.top) / rect.height * sim.world.size)
|
||||
};
|
||||
}
|
||||
function handleMapIntervention(event) {
|
||||
if (!interventionMode || !sim) return;
|
||||
const { x, y } = eventToWorldCoordinates(event);
|
||||
if (x < 0 || y < 0 || x >= sim.world.size || y >= sim.world.size) return;
|
||||
if (interventionMode === "disaster") {
|
||||
const result = sim.triggerManualDisasterAt(x, y);
|
||||
setInterventionStatus(`災害を発生させました(中心 ${result.x}, ${result.y}/半径 ${Math.round(result.radius)})。続けて地点をクリックできます。`);
|
||||
requestRender();
|
||||
drawFrame(true);
|
||||
renderStateGraph();
|
||||
return;
|
||||
}
|
||||
if (interventionMode === "city") {
|
||||
const result = sim.buildCityAt(x, y);
|
||||
if (!result.ok) {
|
||||
setInterventionStatus(result.reason === "water"
|
||||
? "水域には都市を建設できません。陸地をクリックしてください。"
|
||||
: "この地点には都市を建設できません。");
|
||||
return;
|
||||
}
|
||||
setInterventionStatus(`都市${result.city.id}を建設しました(${result.city.x}, ${result.city.y})。続けて建設地点をクリックできます。`);
|
||||
requestRender();
|
||||
drawFrame(true);
|
||||
renderStateGraph();
|
||||
return;
|
||||
}
|
||||
if (interventionMode === "route") {
|
||||
const city = cityAtDisplayTile(x, y);
|
||||
if (!city) {
|
||||
setInterventionStatus("都市をクリックしてください。都市アイコンの範囲内で選択できます。");
|
||||
return;
|
||||
}
|
||||
if (manualRouteStartCityId == null) {
|
||||
manualRouteStartCityId = city.id;
|
||||
setInterventionStatus(`都市${city.id}を選択しました。接続する2つ目の都市をクリックしてください。`);
|
||||
return;
|
||||
}
|
||||
if (manualRouteStartCityId === city.id) {
|
||||
setInterventionStatus("同じ都市は接続できません。別の都市をクリックしてください。");
|
||||
return;
|
||||
}
|
||||
const startId = manualRouteStartCityId;
|
||||
const result = sim.createManualTradeRoute(startId, city.id);
|
||||
manualRouteStartCityId = null;
|
||||
if (!result.ok) {
|
||||
const message = result.reason === "tooFar" ? `都市間が遠すぎます(上限 ${result.maxLength} タイル)。`
|
||||
: result.reason === "noPath" ? "陸路で接続できる経路が見つかりませんでした。"
|
||||
: "交通接続を作成できませんでした。";
|
||||
setInterventionStatus(`${message} 1つ目の都市から選び直してください。`);
|
||||
return;
|
||||
}
|
||||
setInterventionStatus(`都市${startId}と都市${city.id}を接続しました。続けて1つ目の都市をクリックできます。`);
|
||||
requestRender();
|
||||
drawFrame(true);
|
||||
}
|
||||
}
|
||||
|
||||
function reset(size = SimConfig.world?.defaultSize ?? 160) {
|
||||
const initialPopulation = SimConfig.world?.initialPopulation ?? 50000;
|
||||
fpsWindowStartedAt = 0;
|
||||
fpsFrames = 0;
|
||||
if (els.fpsCounter) els.fpsCounter.textContent = "FPS --";
|
||||
els.canvas.width = size;
|
||||
els.canvas.height = size;
|
||||
renderImage = null;
|
||||
renderImageSize = 0;
|
||||
sim = new Simulation(size, initialPopulation);
|
||||
if (els.worldSize) els.worldSize.value = String(size);
|
||||
interventionMode = null;
|
||||
manualRouteStartCityId = null;
|
||||
for (const button of [els.triggerDisaster, els.buildCity, els.connectCities]) button?.setAttribute("aria-pressed", "false");
|
||||
setInterventionStatus("ボタンを選び、地図上をクリックして実行します。");
|
||||
setLegend();
|
||||
drawFrame(true);
|
||||
renderStateGraph();
|
||||
}
|
||||
function openRegenerateDialog() {
|
||||
if (els.worldSize && sim) els.worldSize.value = String(sim.world.size);
|
||||
if (els.regenerateDialog?.showModal) els.regenerateDialog.showModal();
|
||||
}
|
||||
function refreshStateGraph() {
|
||||
renderStateGraph();
|
||||
clearStateGraphInfo();
|
||||
}
|
||||
els.toggleRun.addEventListener("click", () => {
|
||||
running = !running;
|
||||
els.toggleRun.textContent = running ? "Pause" : "Run";
|
||||
els.toggleRun.textContent = running ? "一時停止" : "再開";
|
||||
lastSimTickAt = 0;
|
||||
if (running && loopTimer) {
|
||||
clearTimeout(loopTimer);
|
||||
|
|
@ -72,14 +176,16 @@ scheduleLoop();
|
|||
scheduleLoop();
|
||||
}
|
||||
});
|
||||
els.stepOnce.addEventListener("click", () => {
|
||||
sim.step();
|
||||
requestRender();
|
||||
drawFrame(true);
|
||||
renderStateGraph();
|
||||
els.resetWorld.addEventListener("click", openRegenerateDialog);
|
||||
els.confirmRegenerate?.addEventListener("click", () => {
|
||||
const size = Number(els.worldSize?.value || SimConfig.world?.defaultSize || 160);
|
||||
els.regenerateDialog?.close();
|
||||
reset(size);
|
||||
});
|
||||
els.resetWorld.addEventListener("click", reset);
|
||||
for (const control of [els.worldSize, els.agentCount]) control.addEventListener("change", reset);
|
||||
els.cancelRegenerate?.addEventListener("click", () => els.regenerateDialog?.close());
|
||||
els.triggerDisaster?.addEventListener("click", () => setInterventionMode("disaster"));
|
||||
els.buildCity?.addEventListener("click", () => setInterventionMode("city"));
|
||||
els.connectCities?.addEventListener("click", () => setInterventionMode("route"));
|
||||
els.viewMode.addEventListener("change", () => {
|
||||
setLegend();
|
||||
requestRender();
|
||||
|
|
@ -87,6 +193,7 @@ drawFrame();
|
|||
});
|
||||
els.canvas.addEventListener("mousemove", showTooltip);
|
||||
els.canvas.addEventListener("mouseleave", hideTooltip);
|
||||
els.canvas.addEventListener("click", handleMapIntervention);
|
||||
for (const control of [els.historyFilter, els.historySort, els.historyMetric]) {
|
||||
control?.addEventListener("change", refreshStateGraph);
|
||||
}
|
||||
|
|
@ -100,6 +207,11 @@ graphState.showPast = !graphState.showPast;
|
|||
els.togglePastStates.setAttribute("aria-pressed", String(graphState.showPast));
|
||||
refreshStateGraph();
|
||||
});
|
||||
els.toggleEventIcons?.addEventListener("click", () => {
|
||||
graphState.showEventIcons = !graphState.showEventIcons;
|
||||
els.toggleEventIcons.setAttribute("aria-pressed", String(graphState.showEventIcons));
|
||||
refreshStateGraph();
|
||||
});
|
||||
els.stateGraph?.addEventListener("mousemove", updateStateGraphInfo);
|
||||
els.stateGraph?.addEventListener("mouseleave", clearStateGraphInfo);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
|
|
@ -112,5 +224,5 @@ loopScheduled = false;
|
|||
scheduleLoop();
|
||||
}
|
||||
});
|
||||
reset();
|
||||
reset(SimConfig.world?.defaultSize ?? 160);
|
||||
loop();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue