842 lines
35 KiB
JavaScript
842 lines
35 KiB
JavaScript
"use strict";
|
|
|
|
function bindUI() {
|
|
renderToolPalette();
|
|
loadLogPushSettings();
|
|
syncLogPushControls();
|
|
syncAudioControls();
|
|
applyToolTips();
|
|
renderEcologyCards();
|
|
window.TarinaiSaveSystem?.bindSaveSystem?.();
|
|
window.TarinaiFreezeSystem?.bindFreezeSystem?.();
|
|
uiCache.toolButtons = Array.from(ui.toolPalette?.querySelectorAll(".tool") || []);
|
|
function isActivePlacementTool() {
|
|
const def = typeof toolDefinition === "function" ? toolDefinition(world.tool) : null;
|
|
return Boolean(def?.placeable && typeof toolItemType === "function" && toolItemType(world.tool));
|
|
}
|
|
function clickedInsideGameOrToolUi(target) {
|
|
return Boolean((canvas && canvas.contains?.(target)) || ui.toolPalette?.contains?.(target));
|
|
}
|
|
syncToolSizeBadges();
|
|
for (const toggle of document.querySelectorAll(".panel-card-toggle")) {
|
|
toggle.addEventListener("click", () => {
|
|
const card = toggle.closest(".collapsible-card");
|
|
if (!card) return;
|
|
audio.uiFold?.();
|
|
const collapsed = card.classList.toggle("collapsed");
|
|
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
const state = toggle.querySelector(".panel-card-state");
|
|
if (state) state.textContent = collapsed ? "\u5c55\u958b" : "\u6298\u308a\u7573\u307f";
|
|
});
|
|
}
|
|
ui.colonyChartTabs?.addEventListener("click", (e) => {
|
|
const btn = e.target.closest("button[data-chart]");
|
|
if (!btn) return;
|
|
uiCache.chartMode = btn.dataset.chart || "population";
|
|
for (const b of ui.colonyChartTabs.querySelectorAll("button")) b.classList.toggle("active", b === btn);
|
|
uiCache.lastChartDraw = "";
|
|
uiCache.lastChartDrawAt = 0;
|
|
renderStats();
|
|
});
|
|
syncToolSizeBadges();
|
|
ui.logPushEnabled?.addEventListener("change", () => {
|
|
uiCache.logPushEnabled = Boolean(ui.logPushEnabled.checked);
|
|
audio.notify?.();
|
|
saveLogPushSettings();
|
|
showToast(uiCache.logPushEnabled ? "\u30ed\u30b0\u306e\u30d7\u30c3\u30b7\u30e5\u901a\u77e5\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002" : "\u30ed\u30b0\u306e\u30d7\u30c3\u30b7\u30e5\u901a\u77e5\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002");
|
|
});
|
|
ui.logPushControls?.addEventListener("change", (e) => {
|
|
const input = e.target.closest("[data-push-kind]");
|
|
if (!input) return;
|
|
const kind = input.dataset.pushKind || "";
|
|
audio.uiClick?.();
|
|
if (input.checked) uiCache.logPushKinds.add(kind);
|
|
else uiCache.logPushKinds.delete(kind);
|
|
saveLogPushSettings();
|
|
});
|
|
ui.soundCategoryControls?.addEventListener("change", (e) => {
|
|
const input = e.target.closest("[data-sound-category]");
|
|
if (!input) return;
|
|
const category = input.dataset.soundCategory || "";
|
|
audio.setCategory?.(category, Boolean(input.checked));
|
|
if (audio.enabled && input.checked) audio.uiClick?.();
|
|
syncAudioControls();
|
|
showToast(`${audio.soundCategories?.[category] || category} ${input.checked ? "ON" : "OFF"}`);
|
|
});
|
|
ui.archiveContent?.addEventListener("scroll", scheduleArchiveWindowRender, { passive: true });
|
|
window.addEventListener("scroll", schedulePendingArchiveRender, { passive: true });
|
|
window.addEventListener("resize", schedulePendingArchiveRender, { passive: true });
|
|
const bindArchiveHorizontal = (root) => {
|
|
if (!root) return;
|
|
root.addEventListener("wheel", (e) => {
|
|
const scroller = e.target.closest(".lineage-family-tree, .lineage-main-content");
|
|
if (!scroller) return;
|
|
const canX = scroller.scrollWidth > scroller.clientWidth + 4;
|
|
if (!canX) return;
|
|
const dominant = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : (e.shiftKey ? e.deltaY : 0);
|
|
if (!dominant) return;
|
|
e.preventDefault();
|
|
scroller.scrollLeft += dominant;
|
|
}, { passive: false });
|
|
};
|
|
bindArchiveHorizontal(ui.archiveContent);
|
|
ui.archiveContent?.addEventListener("click", (e) => {
|
|
const node = e.target.closest("[data-family-tarinai-id]");
|
|
if (!node) return;
|
|
const id = node.getAttribute("data-family-tarinai-id");
|
|
const t = world.tarinai.find(x => (x.id === id || x.familyKey === id) && !x.dead);
|
|
if (!t) { showToast("\u305d\u306e\u500b\u4f53\u306f\u3082\u3046\u9078\u629e\u3067\u304d\u307e\u305b\u3093\u3002"); return; }
|
|
world.selected = t;
|
|
t.focusPulseTimer = Math.max(t.focusPulseTimer || 0, 1.15);
|
|
selectTool("observe");
|
|
if (Number.isFinite(t.x) && Number.isFinite(t.y)) {
|
|
world.cameraX = t.x - (world.visibleWorldW ? world.visibleWorldW() : (world.viewportW || world.w)) / 2;
|
|
world.cameraY = t.y - (world.visibleWorldH ? world.visibleWorldH() : (world.viewportH || world.h)) / 2;
|
|
world.clampCamera?.();
|
|
}
|
|
renderStats();
|
|
showToast(`${t.name}\u3092\u9078\u629e\u3057\u307e\u3057\u305f\u3002`);
|
|
});
|
|
|
|
if (!uiCache.archiveAutoRefreshTimer && typeof window !== "undefined") {
|
|
uiCache.archiveAutoRefreshTimer = window.setInterval(() => {
|
|
if (!ui.archiveContent || !archiveAutoUpdateEnabled()) return;
|
|
scheduleArchiveWindowRender();
|
|
}, 5000);
|
|
}
|
|
|
|
ui.selectedInfo?.addEventListener("click", (e) => {
|
|
const categoryToggle = e.target.closest(".selected-category-toggle");
|
|
if (categoryToggle) {
|
|
const category = categoryToggle.closest(".selected-category");
|
|
const id = category?.dataset?.selectedCategory || "";
|
|
if (!category || !id) return;
|
|
if (!uiCache.selectedCollapsedCategories) uiCache.selectedCollapsedCategories = new Set();
|
|
audio.uiFold?.();
|
|
category.classList.toggle("collapsed");
|
|
const collapsed = category.classList.contains("collapsed");
|
|
if (collapsed) uiCache.selectedCollapsedCategories.add(id);
|
|
else uiCache.selectedCollapsedCategories.delete(id);
|
|
categoryToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
return;
|
|
}
|
|
const focusBtn = e.target.closest("[data-focus-tarinai-id]");
|
|
if (focusBtn) {
|
|
e.preventDefault();
|
|
focusTarinaiById(focusBtn.getAttribute("data-focus-tarinai-id"));
|
|
return;
|
|
}
|
|
const btn = e.target.closest("[data-selected-action=\"favorite\"]");
|
|
if (!btn || !world.selected || world.selected.dead) return;
|
|
world.selected.favorite = !world.selected.favorite;
|
|
uiCache.selectedSnapshot = "";
|
|
renderSelected();
|
|
render();
|
|
showToast(world.selected.favorite ? "\u304a\u6c17\u306b\u5165\u308a\u306b\u3057\u307e\u3057\u305f\u3002" : "\u304a\u6c17\u306b\u5165\u308a\u3092\u89e3\u9664\u3057\u307e\u3057\u305f\u3002");
|
|
});
|
|
ui.selectedInfo?.addEventListener("change", (e) => {
|
|
const input = e.target.closest("[data-selected-name]");
|
|
if (!input) return;
|
|
renameSelectedTarinai(input.value);
|
|
});
|
|
ui.selectedInfo?.addEventListener("keydown", (e) => {
|
|
const input = e.target.closest("[data-selected-name]");
|
|
if (!input) return;
|
|
if (e.key === "Enter") { e.preventDefault(); input.blur(); }
|
|
if (e.key === "Escape") { e.preventDefault(); input.value = world.selected?.name || ""; input.blur(); }
|
|
});
|
|
ui.selectedCloseBtn?.addEventListener("click", () => {
|
|
world.selected = null;
|
|
uiCache.selectedSnapshot = "";
|
|
uiCache.selectedEmpty = false;
|
|
renderSelected();
|
|
render();
|
|
});
|
|
|
|
ui.pauseBtn?.addEventListener("click", () => {
|
|
audio.uiClick?.();
|
|
world.paused = !world.paused;
|
|
const archiveStale = uiCache.archiveFamilyVersion !== (world.familyVersion || 0);
|
|
if (archiveAutoUpdateEnabled() && world.paused && (uiCache.archivePendingWhileRunning || archiveStale)) {
|
|
uiCache.archivePendingWhileRunning = false;
|
|
scheduleArchiveWindowRender();
|
|
}
|
|
syncTopButtons();
|
|
});
|
|
ui.speedBtn?.addEventListener("click", () => {
|
|
audio.uiClick?.();
|
|
const speeds = [1, 2, 4];
|
|
const i = speeds.indexOf(world.speed);
|
|
world.speed = speeds[(i + 1) % speeds.length];
|
|
syncTopButtons();
|
|
});
|
|
ui.ecologyBtn?.addEventListener("click", () => { audio.uiClick?.(); openEcologyDialog(); });
|
|
ui.soundBtn?.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
toggleSoundPanel();
|
|
});
|
|
ui.soundPanel?.addEventListener("click", (e) => e.stopPropagation());
|
|
ui.soundMasterToggle?.addEventListener("change", () => {
|
|
const on = audio.setEnabled ? audio.setEnabled(ui.soundMasterToggle.checked) : audio.toggle();
|
|
if (on) audio.uiClick?.();
|
|
syncAudioControls();
|
|
showToast(on ? "\u52b9\u679c\u97f3\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002" : "\u52b9\u679c\u97f3\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002");
|
|
});
|
|
document.addEventListener("click", (e) => {
|
|
if (!ui.soundMenu || ui.soundMenu.contains(e.target)) return;
|
|
closeSoundPanel();
|
|
});
|
|
document.addEventListener("click", (e) => {
|
|
if (!isActivePlacementTool()) return;
|
|
if (clickedInsideGameOrToolUi(e.target)) return;
|
|
selectTool("observe"); // \u9053\u5177\u8a2d\u7f6e\u30e2\u30fc\u30c9\u3092\u89e3\u9664
|
|
});
|
|
ui.creditsBtn?.addEventListener("click", () => { audio.uiClick?.(); openCreditsDialog(); });
|
|
ui.creditsCloseBtn?.addEventListener("click", closeCreditsDialog);
|
|
ui.creditsDialog?.addEventListener("click", (e) => {
|
|
if (e.target === ui.creditsDialog) closeCreditsDialog();
|
|
});
|
|
ui.resetBtn?.addEventListener("click", () => {
|
|
audio.uiClick?.();
|
|
openFieldDialog();
|
|
});
|
|
ui.fieldCancelBtn?.addEventListener("click", closeFieldDialog);
|
|
ui.fieldDialog?.addEventListener("click", (e) => {
|
|
if (e.target === ui.fieldDialog) { closeFieldDialog(); return; }
|
|
const btn = e.target.closest("[data-field-type]");
|
|
if (!btn) return;
|
|
resetWithField(btn.dataset.fieldType || "garden");
|
|
});
|
|
ui.ecologyCloseBtn?.addEventListener("click", closeEcologyDialog);
|
|
ui.ecologyDialog?.addEventListener("click", (e) => {
|
|
if (e.target === ui.ecologyDialog) closeEcologyDialog();
|
|
});
|
|
ui.archiveUpdateToggleBtn?.addEventListener("click", () => { audio.uiClick?.(); setArchiveUpdateEnabled(!archiveAutoUpdateEnabled()); });
|
|
syncArchiveUpdateButton();
|
|
ui.archiveResetBtn?.addEventListener("click", () => { audio.uiClick?.(); openArchiveResetDialog(); });
|
|
ui.archiveResetCancelBtn?.addEventListener("click", closeArchiveResetDialog);
|
|
ui.archiveResetDialog?.addEventListener("click", (e) => {
|
|
if (e.target === ui.archiveResetDialog) closeArchiveResetDialog();
|
|
});
|
|
ui.archiveResetConfirmBtn?.addEventListener("click", () => {
|
|
world.resetFamilyTree?.();
|
|
window.resetArchiveRenderState?.();
|
|
closeArchiveResetDialog();
|
|
renderArchive();
|
|
renderSelected();
|
|
showToast("\u5bb6\u7cfb\u56f3\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u307e\u3057\u305f\u3002");
|
|
});
|
|
|
|
ui.toolPalette?.addEventListener("click", (e) => {
|
|
const toggle = e.target.closest(".tool-category-toggle");
|
|
if (toggle) {
|
|
const category = toggle.closest(".tool-category");
|
|
if (!category) return;
|
|
audio.uiFold?.();
|
|
category.classList.toggle("collapsed");
|
|
toggle.setAttribute("aria-expanded", category.classList.contains("collapsed") ? "false" : "true");
|
|
return;
|
|
}
|
|
const btn = e.target.closest(".tool");
|
|
if (!btn) return;
|
|
const tool = btn.dataset.tool;
|
|
audio.uiClick?.();
|
|
if (world.tool === tool && cycleToolSize(tool)) return;
|
|
selectTool(tool);
|
|
});
|
|
|
|
const chainScroll = (el) => {
|
|
if (!el) return;
|
|
el.addEventListener("wheel", (e) => {
|
|
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
|
const atTop = el.scrollTop <= 0;
|
|
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
|
|
if ((e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) {
|
|
const page = document.scrollingElement || document.documentElement;
|
|
page.scrollTop += e.deltaY;
|
|
}
|
|
}, { passive: true });
|
|
};
|
|
const transferEdgeScroll = (child, parent) => {
|
|
if (!child || !parent) return;
|
|
child.addEventListener("wheel", (e) => {
|
|
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
|
const childCanScroll = child.scrollHeight > child.clientHeight + 1;
|
|
const atTop = child.scrollTop <= 0;
|
|
const atBottom = child.scrollTop + child.clientHeight >= child.scrollHeight - 1;
|
|
const movingInside = childCanScroll && ((e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom));
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (movingInside) {
|
|
child.scrollTop += e.deltaY;
|
|
return;
|
|
}
|
|
parent.scrollTop += e.deltaY;
|
|
}, { passive: false });
|
|
};
|
|
const rightPanel = document.querySelector(".panel");
|
|
chainScroll(rightPanel);
|
|
chainScroll(ui.archiveContent);
|
|
transferEdgeScroll(ui.log, rightPanel);
|
|
|
|
const inputModeManager = window.TarinaiInputMode;
|
|
const setMobileInputMode = (mode = "auto") => {
|
|
mode = inputModeManager?.setMode?.(mode) || (["auto", "camera", "tool", "family"].includes(mode) ? mode : "auto");
|
|
uiCache.mobileInputMode = mode;
|
|
ui.mobileModeControls?.querySelectorAll("[data-mobile-mode]").forEach(btn => btn.classList.toggle("active", btn.dataset.mobileMode === mode));
|
|
document.body.classList.toggle("mobile-mode-camera", mode === "camera");
|
|
document.body.classList.toggle("mobile-mode-tool", mode === "tool");
|
|
document.body.classList.toggle("mobile-mode-family", mode === "family");
|
|
};
|
|
const updateTouchMobileUiClass = () => {
|
|
const enabled = Boolean(inputModeManager?.refresh?.().touchFirst);
|
|
document.body.classList.toggle("touch-mobile-ui", enabled);
|
|
if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled;
|
|
};
|
|
const mobileInputMode = () => inputModeManager?.currentMode || uiCache.mobileInputMode || "auto";
|
|
updateTouchMobileUiClass();
|
|
window.addEventListener("resize", updateTouchMobileUiClass, { passive: true });
|
|
window.matchMedia?.("(pointer: coarse)")?.addEventListener?.("change", updateTouchMobileUiClass);
|
|
setMobileInputMode(uiCache.mobileInputMode || "auto");
|
|
ui.mobileModeControls?.addEventListener("click", (e) => {
|
|
const btn = e.target.closest("[data-mobile-mode]");
|
|
if (!btn) return;
|
|
audio.uiClick?.();
|
|
setMobileInputMode(btn.dataset.mobileMode || "auto");
|
|
showToast(`\u30b9\u30de\u30db\u64cd\u4f5c: ${btn.textContent || btn.dataset.mobileMode}`);
|
|
});
|
|
|
|
const touchPoint = (touches, index = 0) => {
|
|
const t = touches?.[index];
|
|
if (!t) return null;
|
|
return { clientX: t.clientX, clientY: t.clientY };
|
|
};
|
|
const touchCenter = (touches) => {
|
|
const a = touchPoint(touches, 0);
|
|
const b = touchPoint(touches, 1);
|
|
if (!a) return null;
|
|
if (!b) return a;
|
|
return { clientX: (a.clientX + b.clientX) / 2, clientY: (a.clientY + b.clientY) / 2 };
|
|
};
|
|
const touchDistance = (touches) => {
|
|
const a = touchPoint(touches, 0);
|
|
const b = touchPoint(touches, 1);
|
|
if (!a || !b) return 0;
|
|
return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
};
|
|
const screenToWorldClient = (clientX, clientY) => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const sx = clientX - rect.left;
|
|
const sy = clientY - rect.top;
|
|
return world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy, inside: sx >= 0 && sy >= 0 };
|
|
};
|
|
const clientToWorldRaw = (clientX, clientY) => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const sx = clientX - rect.left;
|
|
const sy = clientY - rect.top;
|
|
const scale = world.viewScale ? world.viewScale() : 1;
|
|
const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 };
|
|
const rawX = (sx - off.x) / scale + (world.cameraX || 0);
|
|
const rawY = (sy - off.y) / scale + (world.cameraY || 0);
|
|
return { x: rawX, y: rawY, inside: rawX >= 0 && rawY >= 0 && rawX <= world.w && rawY <= world.h };
|
|
};
|
|
const setZoomAroundClientPoint = (clientX, clientY, nextZoom) => {
|
|
if (!world.setFieldZoom) return false;
|
|
const rect = canvas.getBoundingClientRect();
|
|
const sx = clientX - rect.left;
|
|
const sy = clientY - rect.top;
|
|
const focus = world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy };
|
|
if (!world.setFieldZoom(nextZoom)) return false;
|
|
const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 };
|
|
const scale = world.viewScale ? world.viewScale() : 1;
|
|
world.cameraX = focus.x - (sx - off.x) / scale;
|
|
world.cameraY = focus.y - (sy - off.y) / scale;
|
|
world.clampCamera?.();
|
|
return true;
|
|
};
|
|
|
|
function isClientOverFrozenPanel(clientX, clientY) {
|
|
const panel = document.getElementById("frozenPanel");
|
|
if (!panel) return false;
|
|
const r = panel.getBoundingClientRect();
|
|
return clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom;
|
|
}
|
|
|
|
function rememberGrabOrigin(target) {
|
|
if (!target || uiCache.grabKind !== "tarinai") return;
|
|
uiCache.grabOrigin = { x: target.x, y: target.y, vx: target.vx || 0, vy: target.vy || 0 };
|
|
}
|
|
|
|
function finishTarinaiGrab(target, clientX, clientY) {
|
|
if (!target || uiCache.grabKind !== "tarinai") return false;
|
|
if (isClientOverFrozenPanel(clientX, clientY) && window.TarinaiFreezeSystem?.freezeTarinai) {
|
|
window.TarinaiFreezeSystem.freezeTarinai(target, world);
|
|
uiCache.grabOrigin = null;
|
|
return true;
|
|
}
|
|
const p = screenToWorldClient(clientX, clientY);
|
|
if (!p.inside && uiCache.grabOrigin) {
|
|
target.x = uiCache.grabOrigin.x;
|
|
target.y = uiCache.grabOrigin.y;
|
|
target.vx = uiCache.grabOrigin.vx || 0;
|
|
target.vy = uiCache.grabOrigin.vy || 0;
|
|
target.thought = "\u30d5\u30a3\u30fc\u30eb\u30c9\u306b\u623b\u3055\u308c\u305f";
|
|
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
|
showToast("\u30d5\u30a3\u30fc\u30eb\u30c9\u5916\u306a\u306e\u3067\u5143\u306e\u4f4d\u7f6e\u306b\u623b\u3057\u307e\u3057\u305f\u3002");
|
|
uiCache.grabOrigin = null;
|
|
return true;
|
|
}
|
|
uiCache.grabOrigin = null;
|
|
return false;
|
|
}
|
|
const beginTouchGrab = (p, e) => {
|
|
const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null;
|
|
if (!target) return false;
|
|
audio.grab?.();
|
|
uiCache.grabbing = true;
|
|
uiCache.grabMoved = false;
|
|
uiCache.grabTarget = target;
|
|
uiCache.grabKind = target instanceof Tarinai ? "tarinai" : "item";
|
|
rememberGrabOrigin(target);
|
|
uiCache.grabLastWorldX = p.x;
|
|
uiCache.grabLastWorldY = p.y;
|
|
uiCache.grabStartX = e.touches?.[0]?.clientX || 0;
|
|
uiCache.grabStartY = e.touches?.[0]?.clientY || 0;
|
|
if (uiCache.grabKind === "tarinai") {
|
|
const lodged = target.currentLodgedPin?.() || (world.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === target.id);
|
|
if (lodged?.detachPushpin) lodged.detachPushpin(world, "pinch");
|
|
target.sleeping = false;
|
|
if (target.state === "sleep" || target.state === "seek_bed") target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
|
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18);
|
|
target.thought = "\u3064\u307e\u307e\u308c\u3066\u3044\u308b";
|
|
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 1.2);
|
|
} else {
|
|
if (isPinType(target.type) && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch");
|
|
target.dropTimer = 0;
|
|
target.dropMax = 0;
|
|
target.dropImpactDone = true;
|
|
}
|
|
uiCache.touchMode = "grab";
|
|
renderStats();
|
|
return true;
|
|
};
|
|
|
|
canvas?.addEventListener("touchstart", (e) => {
|
|
if (!e.touches?.length) return;
|
|
e.preventDefault();
|
|
uiCache.touchMoved = false;
|
|
const mode = mobileInputMode();
|
|
if ((mode === "camera" || mode === "family") && e.touches.length === 1) {
|
|
const t = e.touches[0];
|
|
uiCache.touchStartX = t.clientX;
|
|
uiCache.touchStartY = t.clientY;
|
|
uiCache.touchLastX = t.clientX;
|
|
uiCache.touchLastY = t.clientY;
|
|
uiCache.touchMode = "cameraPan";
|
|
return;
|
|
}
|
|
if (e.touches.length >= 2) {
|
|
const c = touchCenter(e.touches);
|
|
uiCache.touchMode = "pinchZoom";
|
|
uiCache.pinchStartDistance = Math.max(1, touchDistance(e.touches));
|
|
uiCache.pinchStartZoom = world.fieldZoom || 1;
|
|
uiCache.pinchLastCenterX = c?.clientX || 0;
|
|
uiCache.pinchLastCenterY = c?.clientY || 0;
|
|
return;
|
|
}
|
|
const t = e.touches[0];
|
|
const p = screenToWorldClient(t.clientX, t.clientY);
|
|
uiCache.touchStartX = t.clientX;
|
|
uiCache.touchStartY = t.clientY;
|
|
uiCache.touchLastX = t.clientX;
|
|
uiCache.touchLastY = t.clientY;
|
|
uiCache.touchLastWorldX = p.x;
|
|
uiCache.touchLastWorldY = p.y;
|
|
if (mode === "tool" && world.tool === "observe") {
|
|
uiCache.touchMode = "toolTapOnly";
|
|
return;
|
|
}
|
|
if (world.tool === "water_hose" && p.inside) {
|
|
audio.waterHose?.();
|
|
uiCache.hosing = true;
|
|
uiCache.hoseMoved = false;
|
|
uiCache.hoseLastWorldX = p.x;
|
|
uiCache.hoseLastWorldY = p.y;
|
|
uiCache.hoseStartX = t.clientX;
|
|
uiCache.hoseStartY = t.clientY;
|
|
world.applyWaterHose?.(p.x, p.y, 0, 0, 0.10);
|
|
uiCache.touchMode = "hose";
|
|
render();
|
|
return;
|
|
}
|
|
if (world.tool === "pinch" && p.inside && beginTouchGrab(p, e)) return;
|
|
uiCache.touchMode = mode === "tool" ? "toolTapOnly" : "panOrTap";
|
|
}, { passive: false });
|
|
|
|
canvas?.addEventListener("touchmove", (e) => {
|
|
if (!e.touches?.length || !uiCache.touchMode) return;
|
|
e.preventDefault();
|
|
if (uiCache.touchMode === "pinchZoom" && e.touches.length >= 2) {
|
|
const c = touchCenter(e.touches);
|
|
const distNow = Math.max(1, touchDistance(e.touches));
|
|
const ratio = distNow / Math.max(1, uiCache.pinchStartDistance || distNow);
|
|
setZoomAroundClientPoint(c.clientX, c.clientY, (uiCache.pinchStartZoom || 1) * ratio);
|
|
const dx = c.clientX - (uiCache.pinchLastCenterX || c.clientX);
|
|
const dy = c.clientY - (uiCache.pinchLastCenterY || c.clientY);
|
|
uiCache.pinchLastCenterX = c.clientX;
|
|
uiCache.pinchLastCenterY = c.clientY;
|
|
if (Math.hypot(dx, dy) > 0.1) world.panCamera?.(-dx, -dy);
|
|
uiCache.touchMoved = true;
|
|
render();
|
|
return;
|
|
}
|
|
const t = e.touches[0];
|
|
const p = screenToWorldClient(t.clientX, t.clientY);
|
|
const screenDx = t.clientX - (uiCache.touchLastX || t.clientX);
|
|
const screenDy = t.clientY - (uiCache.touchLastY || t.clientY);
|
|
const totalMove = Math.hypot(t.clientX - (uiCache.touchStartX || t.clientX), t.clientY - (uiCache.touchStartY || t.clientY));
|
|
uiCache.touchLastX = t.clientX;
|
|
uiCache.touchLastY = t.clientY;
|
|
if (uiCache.touchMode === "cameraPan") {
|
|
if (totalMove > 2) uiCache.touchMoved = true;
|
|
world.panCamera?.(-screenDx, -screenDy);
|
|
render();
|
|
return;
|
|
}
|
|
if (uiCache.touchMode === "toolTapOnly") {
|
|
if (totalMove > 8) uiCache.touchMoved = true;
|
|
return;
|
|
}
|
|
if (uiCache.touchMode === "hose" && uiCache.hosing) {
|
|
const dx = p.x - (uiCache.hoseLastWorldX ?? p.x);
|
|
const dy = p.y - (uiCache.hoseLastWorldY ?? p.y);
|
|
uiCache.hoseLastWorldX = p.x;
|
|
uiCache.hoseLastWorldY = p.y;
|
|
if (totalMove > 3) { uiCache.hoseMoved = true; uiCache.touchMoved = true; }
|
|
if (p.inside) world.applyWaterHose?.(p.x, p.y, dx, dy, 0.10);
|
|
render();
|
|
return;
|
|
}
|
|
if (uiCache.touchMode === "grab" && uiCache.grabbing) {
|
|
const target = uiCache.grabTarget;
|
|
if (!target || target.dead) {
|
|
uiCache.grabbing = false;
|
|
uiCache.grabTarget = null;
|
|
uiCache.touchMode = "";
|
|
return;
|
|
}
|
|
const dx = p.x - (uiCache.grabLastWorldX ?? p.x);
|
|
const dy = p.y - (uiCache.grabLastWorldY ?? p.y);
|
|
uiCache.grabLastWorldX = p.x;
|
|
uiCache.grabLastWorldY = p.y;
|
|
if (totalMove > 3) { uiCache.grabMoved = true; uiCache.touchMoved = true; }
|
|
const pad = target.radius || target.r || 16;
|
|
if (uiCache.grabKind === "tarinai" && !p.inside) {
|
|
const raw = clientToWorldRaw(e.touches?.[0]?.clientX || uiCache.touchLastX || 0, e.touches?.[0]?.clientY || uiCache.touchLastY || 0);
|
|
target.x = raw.x;
|
|
target.y = raw.y;
|
|
} else {
|
|
target.x = clamp(p.x, CONFIG.worldPadding + pad * 0.15, world.w - CONFIG.worldPadding - pad * 0.15);
|
|
target.y = clamp(p.y, CONFIG.worldPadding + pad * 0.15, world.h - CONFIG.worldPadding - pad * 0.15);
|
|
}
|
|
target.vx = clamp(dx * 16, -160, 160);
|
|
target.vy = clamp(dy * 16, -160, 160);
|
|
if (uiCache.grabKind === "tarinai") {
|
|
target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
|
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
|
} else if (target.type === "ball") {
|
|
target.prevX = target.x - dx;
|
|
target.prevY = target.y - dy;
|
|
target.spinVelocity = clamp((target.spinVelocity || 0) + dx * 0.06, -38, 38);
|
|
}
|
|
world.drawListDirty = true;
|
|
const now = performance.now();
|
|
if (!uiCache.grabLastSpatialAt || now - uiCache.grabLastSpatialAt > 80) {
|
|
world.rebuildSpatial?.();
|
|
uiCache.grabLastSpatialAt = now;
|
|
}
|
|
render();
|
|
return;
|
|
}
|
|
if (uiCache.touchMode === "panOrTap") {
|
|
if (totalMove > 7) {
|
|
uiCache.touchMoved = true;
|
|
world.panCamera?.(-screenDx, -screenDy);
|
|
render();
|
|
}
|
|
}
|
|
}, { passive: false });
|
|
|
|
canvas?.addEventListener("touchend", (e) => {
|
|
e.preventDefault();
|
|
if (uiCache.hosing) {
|
|
uiCache.hosing = false;
|
|
world.rebuildSpatial?.();
|
|
renderStats();
|
|
}
|
|
if (uiCache.grabbing) {
|
|
const target = uiCache.grabTarget;
|
|
if (target) {
|
|
if (uiCache.grabKind === "tarinai") {
|
|
target.target = null;
|
|
target.targetKey = "";
|
|
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15);
|
|
finishTarinaiGrab(target, e.changedTouches?.[0]?.clientX || uiCache.touchLastX || 0, e.changedTouches?.[0]?.clientY || uiCache.touchLastY || 0);
|
|
} else if (target.type === "ball") {
|
|
target.prevX = target.x;
|
|
target.prevY = target.y;
|
|
}
|
|
}
|
|
audio.drop?.();
|
|
uiCache.grabbing = false;
|
|
uiCache.grabTarget = null;
|
|
uiCache.grabKind = "";
|
|
world.rebuildSpatial?.();
|
|
uiCache.grabLastSpatialAt = 0;
|
|
renderStats();
|
|
} else if (uiCache.touchMode === "toolTapOnly" && !uiCache.touchMoved) {
|
|
const p = screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
|
if (p.inside) {
|
|
world.handleClick(p.x, p.y);
|
|
renderStats();
|
|
}
|
|
} else if (uiCache.touchMode === "panOrTap" && !uiCache.touchMoved) {
|
|
const p = screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
|
if (p.inside) {
|
|
world.handleClick(p.x, p.y);
|
|
renderStats();
|
|
}
|
|
}
|
|
if (!e.touches?.length) {
|
|
uiCache.touchMode = "";
|
|
uiCache.touchMoved = false;
|
|
}
|
|
}, { passive: false });
|
|
|
|
canvas?.addEventListener("touchcancel", () => {
|
|
uiCache.touchMode = "";
|
|
uiCache.touchMoved = false;
|
|
uiCache.hosing = false;
|
|
uiCache.grabbing = false;
|
|
uiCache.grabTarget = null;
|
|
uiCache.grabKind = "";
|
|
}, { passive: true });
|
|
|
|
|
|
canvas?.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
canvas?.addEventListener("mousedown", (e) => {
|
|
if (e.button === 0 && world.tool === "water_hose") {
|
|
audio.waterHose?.();
|
|
const p = screenToWorld(e);
|
|
if (!p.inside) return;
|
|
e.preventDefault();
|
|
uiCache.hosing = true;
|
|
uiCache.hoseMoved = false;
|
|
uiCache.hoseLastWorldX = p.x;
|
|
uiCache.hoseLastWorldY = p.y;
|
|
uiCache.hoseStartX = e.clientX;
|
|
uiCache.hoseStartY = e.clientY;
|
|
world.applyWaterHose?.(p.x, p.y, 0, 0, 0.10);
|
|
render();
|
|
return;
|
|
}
|
|
if (e.button === 0 && world.tool === "pinch") {
|
|
const p = screenToWorld(e);
|
|
if (!p.inside) return;
|
|
const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null;
|
|
if (!target) {
|
|
showToast("\u3064\u307e\u3081\u308b\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002");
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
audio.grab?.();
|
|
uiCache.grabbing = true;
|
|
uiCache.grabMoved = false;
|
|
uiCache.grabTarget = target;
|
|
uiCache.grabKind = target instanceof Tarinai ? "tarinai" : "item";
|
|
rememberGrabOrigin(target);
|
|
uiCache.grabLastWorldX = p.x;
|
|
uiCache.grabLastWorldY = p.y;
|
|
uiCache.grabStartX = e.clientX;
|
|
uiCache.grabStartY = e.clientY;
|
|
if (uiCache.grabKind === "tarinai") {
|
|
const lodged = target.currentLodgedPin?.() || (world.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === target.id);
|
|
if (lodged?.detachPushpin) lodged.detachPushpin(world, "pinch");
|
|
target.sleeping = false;
|
|
if (target.state === "sleep" || target.state === "seek_bed") target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
|
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18);
|
|
target.thought = "\u3064\u307e\u307e\u308c\u3066\u3044\u308b";
|
|
target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 1.2);
|
|
} else {
|
|
if (isPinType(target.type) && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch");
|
|
target.dropTimer = 0;
|
|
target.dropMax = 0;
|
|
target.dropImpactDone = true;
|
|
}
|
|
canvas.style.cursor = "grabbing";
|
|
renderStats();
|
|
return;
|
|
}
|
|
if (e.button !== 2) return;
|
|
e.preventDefault();
|
|
uiCache.panning = true;
|
|
uiCache.panMoved = false;
|
|
uiCache.panButton = e.button;
|
|
uiCache.panStartX = e.clientX;
|
|
uiCache.panStartY = e.clientY;
|
|
uiCache.panLastX = e.clientX;
|
|
uiCache.panLastY = e.clientY;
|
|
});
|
|
window.addEventListener("mouseup", (e) => {
|
|
if (uiCache.hosing) {
|
|
uiCache.hosing = false;
|
|
canvas.style.cursor = world.tool === "water_hose" ? "crosshair" : (world.tool === "observe" ? "default" : (world.tool === "pinch" ? "grab" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair")));
|
|
world.rebuildSpatial?.();
|
|
renderStats();
|
|
return;
|
|
}
|
|
if (uiCache.grabbing) {
|
|
const target = uiCache.grabTarget;
|
|
if (target) {
|
|
if (uiCache.grabKind === "tarinai") {
|
|
target.target = null;
|
|
target.targetKey = "";
|
|
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15);
|
|
finishTarinaiGrab(target, e.clientX, e.clientY);
|
|
} else if (target.type === "ball") {
|
|
target.prevX = target.x;
|
|
target.prevY = target.y;
|
|
}
|
|
}
|
|
audio.drop?.();
|
|
uiCache.grabbing = false;
|
|
uiCache.grabTarget = null;
|
|
uiCache.grabKind = "";
|
|
world.rebuildSpatial?.();
|
|
uiCache.grabLastSpatialAt = 0;
|
|
canvas.style.cursor = world.tool === "pinch" ? "grab" : (world.tool === "observe" ? "default" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair"));
|
|
renderStats();
|
|
return;
|
|
}
|
|
if (!uiCache.panning) return;
|
|
if (uiCache.panButton !== null && e.button !== uiCache.panButton) return;
|
|
uiCache.panning = false;
|
|
uiCache.panButton = null;
|
|
canvas.style.cursor = world.tool === "observe" ? "default" : (world.tool === "pinch" ? "grab" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair"));
|
|
});
|
|
window.addEventListener("mousemove", (e) => {
|
|
if (uiCache.hosing) {
|
|
const p = screenToWorld(e);
|
|
const dx = p.x - (uiCache.hoseLastWorldX ?? p.x);
|
|
const dy = p.y - (uiCache.hoseLastWorldY ?? p.y);
|
|
uiCache.hoseLastWorldX = p.x;
|
|
uiCache.hoseLastWorldY = p.y;
|
|
const totalMove = Math.hypot(e.clientX - (uiCache.hoseStartX || e.clientX), e.clientY - (uiCache.hoseStartY || e.clientY));
|
|
if (totalMove > 3) uiCache.hoseMoved = true;
|
|
if (p.inside) world.applyWaterHose?.(p.x, p.y, dx, dy, 0.10);
|
|
canvas.style.cursor = "crosshair";
|
|
render();
|
|
return;
|
|
}
|
|
if (uiCache.grabbing) {
|
|
const target = uiCache.grabTarget;
|
|
if (!target || target.dead) {
|
|
uiCache.grabbing = false;
|
|
uiCache.grabTarget = null;
|
|
canvas.style.cursor = world.tool === "pinch" ? "grab" : "default";
|
|
return;
|
|
}
|
|
const p = screenToWorld(e);
|
|
const dx = p.x - (uiCache.grabLastWorldX ?? p.x);
|
|
const dy = p.y - (uiCache.grabLastWorldY ?? p.y);
|
|
uiCache.grabLastWorldX = p.x;
|
|
uiCache.grabLastWorldY = p.y;
|
|
const totalMove = Math.hypot(e.clientX - (uiCache.grabStartX || e.clientX), e.clientY - (uiCache.grabStartY || e.clientY));
|
|
if (totalMove > 3) uiCache.grabMoved = true;
|
|
const pad = target.radius || target.r || 16;
|
|
if (uiCache.grabKind === "tarinai" && !p.inside) {
|
|
const raw = clientToWorldRaw(e.clientX, e.clientY);
|
|
target.x = raw.x;
|
|
target.y = raw.y;
|
|
} else {
|
|
target.x = clamp(p.x, CONFIG.worldPadding + pad * 0.15, world.w - CONFIG.worldPadding - pad * 0.15);
|
|
target.y = clamp(p.y, CONFIG.worldPadding + pad * 0.15, world.h - CONFIG.worldPadding - pad * 0.15);
|
|
}
|
|
target.vx = clamp(dx * 16, -160, 160);
|
|
target.vy = clamp(dy * 16, -160, 160);
|
|
if (uiCache.grabKind === "tarinai") {
|
|
target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
|
} else if (target.type === "ball") {
|
|
target.prevX = target.x - dx;
|
|
target.prevY = target.y - dy;
|
|
target.spinVelocity = clamp((target.spinVelocity || 0) + dx * 0.06, -38, 38);
|
|
}
|
|
world.drawListDirty = true;
|
|
const now = performance.now();
|
|
if (!uiCache.grabLastSpatialAt || now - uiCache.grabLastSpatialAt > 80) {
|
|
world.rebuildSpatial?.();
|
|
uiCache.grabLastSpatialAt = now;
|
|
}
|
|
canvas.style.cursor = "grabbing";
|
|
return;
|
|
}
|
|
if (!uiCache.panning) return;
|
|
const dx = e.clientX - uiCache.panLastX;
|
|
const dy = e.clientY - uiCache.panLastY;
|
|
uiCache.panLastX = e.clientX;
|
|
uiCache.panLastY = e.clientY;
|
|
const totalMove = Math.hypot(e.clientX - uiCache.panStartX, e.clientY - uiCache.panStartY);
|
|
if (totalMove > 8) {
|
|
canvas.style.cursor = "grabbing";
|
|
world.panCamera?.(-dx, -dy);
|
|
uiCache.panMoved = true;
|
|
render();
|
|
}
|
|
});
|
|
|
|
canvas?.addEventListener("click", (e) => {
|
|
if (uiCache.hoseMoved) {
|
|
uiCache.hoseMoved = false;
|
|
return;
|
|
}
|
|
if (uiCache.grabMoved) {
|
|
uiCache.grabMoved = false;
|
|
return;
|
|
}
|
|
if (uiCache.panMoved) {
|
|
uiCache.panMoved = false;
|
|
return;
|
|
}
|
|
const p = screenToWorld(e);
|
|
if (!p.inside) return;
|
|
world.handleClick(p.x, p.y);
|
|
renderStats();
|
|
});
|
|
|
|
canvas?.addEventListener("mousemove", (e) => {
|
|
const p = screenToWorld(e);
|
|
const prevX = world.pointer.x;
|
|
const prevY = world.pointer.y;
|
|
world.pointer.x = p.x;
|
|
world.pointer.y = p.y;
|
|
world.pointer.inside = !!p.inside;
|
|
const delta = Math.hypot((p.x || 0) - (prevX || 0), (p.y || 0) - (prevY || 0));
|
|
world.pointer.motion = clamp(delta / 18, 0, 2);
|
|
world.pointer.movedAt = performance.now();
|
|
});
|
|
canvas?.addEventListener("wheel", resizeFieldFromWheel, { passive: false });
|
|
canvas?.addEventListener("mouseleave", () => {
|
|
world.pointer.inside = false;
|
|
world.pointer.motion = 0;
|
|
});
|
|
|
|
window.addEventListener("keydown", (e) => {
|
|
if (e.key !== "Escape") return;
|
|
closeFieldDialog();
|
|
closeEcologyDialog();
|
|
closeArchiveResetDialog();
|
|
closeCreditsDialog();
|
|
});
|
|
|
|
window.addEventListener("resize", resizeCanvas);
|
|
}
|