711 lines
31 KiB
JavaScript
711 lines
31 KiB
JavaScript
"use strict";
|
|
|
|
function bindUI() {
|
|
renderToolPalette();
|
|
loadLogPushSettings();
|
|
syncLogPushControls();
|
|
syncAudioControls();
|
|
window.TarinaiTooltips.applyToolPalette();
|
|
window.TarinaiSaveSystem.bindSaveSystem();
|
|
uiCache.toolButtons = Array.from(ui.toolPalette?.querySelectorAll(".tool") || []);
|
|
function isActiveToolMode() {
|
|
return Boolean(world?.tool && world.tool !== "observe");
|
|
}
|
|
function clickedInsideGameOrToolUi(target) {
|
|
return Boolean((canvas && canvas.contains?.(target)) || ui.toolPalette?.contains?.(target) || ui.panelQuickTabs?.contains?.(target) || ui.mobileModeControls?.contains?.(target) || ui.mobileRotateControls?.contains?.(target) || ui.backToGameBtn?.contains?.(target));
|
|
}
|
|
|
|
function panelSectionForTab(tab = "") {
|
|
if (tab === "game") return document.querySelector(".stage-wrap") || canvas;
|
|
if (tab === "selected") return ui.selectedCard;
|
|
if (tab === "tools") return ui.toolCard;
|
|
if (tab === "colony") return document.querySelector(".colony-card");
|
|
if (tab === "events") return document.querySelector(".log-card");
|
|
if (tab === "family") return document.querySelector(".lineage-main-card");
|
|
return null;
|
|
}
|
|
|
|
function setActivePanelTab(tab = "") {
|
|
for (const btn of ui.panelQuickTabs?.querySelectorAll("[data-panel-tab]") || []) {
|
|
btn.classList.toggle("active", btn.dataset.panelTab === tab);
|
|
}
|
|
}
|
|
|
|
function isTouchMobileLayout() {
|
|
return Boolean(window.TarinaiInputMode.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
|
}
|
|
|
|
function isDesktopLayout() {
|
|
return window.matchMedia?.("(min-width: 981px)")?.matches ?? window.innerWidth >= 981;
|
|
}
|
|
|
|
function syncVisualSettingsDialog() {
|
|
const settings = window.TarinaiPerf?.visualSettings?.() || {};
|
|
ui.visualSettingsDialog?.querySelectorAll("[data-visual-setting]").forEach((btn) => {
|
|
const active = settings[btn.dataset.visualSetting] === btn.dataset.visualValue;
|
|
btn.classList.toggle("active", active);
|
|
btn.setAttribute("aria-pressed", active ? "true" : "false");
|
|
});
|
|
}
|
|
|
|
function openVisualSettingsDialog() {
|
|
syncVisualSettingsDialog();
|
|
ui.visualSettingsDialog?.classList.remove("hidden");
|
|
}
|
|
|
|
function closeVisualSettingsDialog() {
|
|
ui.visualSettingsDialog?.classList.add("hidden");
|
|
}
|
|
|
|
function markCollapsibleCardState(card, collapsed, { user = false } = {}) {
|
|
if (!card) return;
|
|
card.classList.toggle("collapsed", Boolean(collapsed));
|
|
if (user) {
|
|
if (collapsed) card.dataset.userCollapsed = "1";
|
|
else delete card.dataset.userCollapsed;
|
|
}
|
|
const toggle = card.querySelector(".panel-card-toggle");
|
|
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";
|
|
}
|
|
|
|
function markToolCategoryState(category, collapsed, { user = false } = {}) {
|
|
if (!category) return;
|
|
category.classList.toggle("collapsed", Boolean(collapsed));
|
|
if (user) {
|
|
if (collapsed) category.dataset.userCollapsed = "1";
|
|
else delete category.dataset.userCollapsed;
|
|
}
|
|
category.querySelector(".tool-category-toggle")?.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
}
|
|
|
|
|
|
function scrollWindowToElement(target, offset = 0, behavior = "auto") {
|
|
if (!target) return;
|
|
const scroller = document.scrollingElement || document.documentElement;
|
|
const rect = target.getBoundingClientRect?.();
|
|
if (!rect) return;
|
|
const maxTop = Math.max(0, scroller.scrollHeight - window.innerHeight);
|
|
const top = Math.max(0, Math.min(window.scrollY + rect.top - offset, maxTop));
|
|
window.scrollTo({ top, behavior: "auto" });
|
|
}
|
|
|
|
function scrollPanelToTab(tab = "") {
|
|
const panel = ui.panel;
|
|
if (!panel) return false;
|
|
window.TarinaiTooltips.hide();
|
|
if (tab === "selected" && (!world.selected || world.selected.dead || !world.tarinai.includes(world.selected))) {
|
|
selectTool("observe");
|
|
uiCache.showSelectedEmpty = true;
|
|
uiCache.selectedEmpty = false;
|
|
renderSelected();
|
|
showToast("\u305F\u308A\u306A\u3044\u304C\u672A\u9078\u629E\u3067\u3059\u3002\u305F\u308A\u306A\u3044\u30C7\u30FC\u30BF\u3092\u958B\u3051\u308B\u72B6\u614B\u306B\u3057\u307E\u3057\u305F\u3002");
|
|
}
|
|
const target = panelSectionForTab(tab);
|
|
if (!target) return false;
|
|
target.classList?.remove?.("hidden");
|
|
if (tab === "family") {
|
|
// \u66F4\u65B0OFF\u3067\u3082\u3001\u30E6\u30FC\u30B6\u30FC\u304C\u5BB6\u7CFB\u56F3\u3078\u79FB\u52D5\u3057\u305F\u6642\u3060\u3051\u73FE\u5728\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u3092\u63CF\u753B\u3059\u308B\u3002
|
|
window.resetArchiveRenderState?.();
|
|
renderArchive?.();
|
|
}
|
|
const isDesktop = window.matchMedia?.("(min-width: 981px)")?.matches ?? window.innerWidth >= 981;
|
|
if (isDesktop && tab !== "game" && tab !== "family") {
|
|
const tabsH = ui.panelQuickTabs?.offsetHeight || 0;
|
|
const top = Math.max(0, target.offsetTop - tabsH - 8);
|
|
panel.scrollTo({ top, behavior: "auto" });
|
|
} else {
|
|
// \u30B9\u30DE\u30DB\u306F\u30DA\u30FC\u30B8\u5168\u4F53\u30B9\u30AF\u30ED\u30FC\u30EB\u3002\u76EE\u7684\u5730UI\u306E\u4E0A\u7AEF\u3092\u753B\u9762\u4E0A\u7AEF\u3078\u5408\u308F\u305B\u308B\u3002
|
|
scrollWindowToElement(target, 0, "auto");
|
|
}
|
|
setActivePanelTab(tab);
|
|
return true;
|
|
}
|
|
|
|
const activatePanelQuickTab = (e) => {
|
|
const btn = e.target.closest("[data-panel-tab]");
|
|
if (!btn) return;
|
|
const now = performance?.now?.() ?? Date.now();
|
|
if (now - Number(ui.panelQuickTabs?.dataset?.lastActivateAt || 0) < 50) return;
|
|
if (ui.panelQuickTabs) ui.panelQuickTabs.dataset.lastActivateAt = String(now);
|
|
e.preventDefault();
|
|
audio.uiClick?.();
|
|
scrollPanelToTab(btn.dataset.panelTab || "");
|
|
};
|
|
|
|
ui.panelQuickTabs?.addEventListener("click", activatePanelQuickTab);
|
|
ui.panelQuickTabs?.addEventListener("pointerup", activatePanelQuickTab, { passive: false });
|
|
ui.panelQuickTabs?.addEventListener("touchend", activatePanelQuickTab, { passive: false });
|
|
|
|
const applyTemperatureSlider = () => {
|
|
if (!world || !ui.temperatureSlider) return;
|
|
world.setManualTemperature?.(Number(ui.temperatureSlider.value));
|
|
world.setTemperatureAuto?.(false);
|
|
renderStats?.();
|
|
};
|
|
ui.temperatureAutoToggle?.addEventListener("change", () => {
|
|
if (!world) return;
|
|
world.setTemperatureAuto?.(ui.temperatureAutoToggle.checked);
|
|
if (ui.temperatureSlider && world.temperatureAuto === false) world.setManualTemperature?.(Number(ui.temperatureSlider.value));
|
|
audio.uiClick?.();
|
|
renderStats?.();
|
|
});
|
|
ui.temperatureSlider?.addEventListener("input", applyTemperatureSlider);
|
|
ui.temperatureSlider?.addEventListener("change", applyTemperatureSlider);
|
|
|
|
const colonyLimitControls = (kind) => kind === "tarinai"
|
|
? { slider: ui.tarinaiPopulationLimitSlider, input: ui.tarinaiPopulationLimitInput }
|
|
: { slider: ui.objectLimitSlider, input: ui.objectLimitInput };
|
|
const syncColonyLimitControls = (kind, value) => {
|
|
const { slider, input } = colonyLimitControls(kind);
|
|
const limit = Math.max(0, Math.floor(Number(value || 0) || 0));
|
|
if (input && document.activeElement !== input) input.value = limit > 0 ? String(limit) : "";
|
|
if (slider && document.activeElement !== slider) {
|
|
const max = Math.max(0, Number(slider.max || 0) || 0);
|
|
slider.value = String(max > 0 ? Math.min(limit, max) : limit);
|
|
slider.setAttribute("aria-valuetext", limit > 0 ? String(limit) : "\u4e0a\u9650\u306a\u3057");
|
|
}
|
|
};
|
|
window.syncColonyLimitControls = syncColonyLimitControls;
|
|
const applyColonyLimit = (kind, source = "number") => {
|
|
if (!world) return;
|
|
const { slider, input } = colonyLimitControls(kind);
|
|
const sourceEl = String(source).startsWith("slider") ? slider : input;
|
|
if (!sourceEl) return;
|
|
const raw = sourceEl.value?.trim?.() === "" ? 0 : Number(sourceEl.value);
|
|
const value = Math.max(0, Math.floor(Number(raw) || 0));
|
|
if (kind === "tarinai") world.setTarinaiPopulationLimit?.(value);
|
|
else world.setObjectLimit?.(value);
|
|
syncColonyLimitControls(kind, value);
|
|
if (source !== "slider-live") audio.uiClick?.();
|
|
renderStats?.();
|
|
};
|
|
ui.tarinaiPopulationLimitSlider?.addEventListener("input", () => applyColonyLimit("tarinai", "slider-live"));
|
|
ui.tarinaiPopulationLimitSlider?.addEventListener("change", () => applyColonyLimit("tarinai", "slider"));
|
|
ui.objectLimitSlider?.addEventListener("input", () => applyColonyLimit("object", "slider-live"));
|
|
ui.objectLimitSlider?.addEventListener("change", () => applyColonyLimit("object", "slider"));
|
|
ui.tarinaiPopulationLimitInput?.addEventListener("change", () => applyColonyLimit("tarinai", "number"));
|
|
ui.objectLimitInput?.addEventListener("change", () => applyColonyLimit("object", "number"));
|
|
|
|
ui.panel?.addEventListener("scroll", () => {
|
|
const panel = ui.panel;
|
|
if (!panel || !ui.panelQuickTabs) return;
|
|
const tabsH = ui.panelQuickTabs.offsetHeight || 0;
|
|
const entries = [
|
|
["game", document.querySelector(".stage-wrap") || canvas],
|
|
["selected", ui.selectedCard],
|
|
["tools", ui.toolCard],
|
|
["colony", document.querySelector(".colony-card")],
|
|
["events", document.querySelector(".log-card")],
|
|
["family", document.querySelector(".lineage-main-card")],
|
|
].filter(([, el]) => el && !el.classList.contains("hidden"));
|
|
let active = entries[0]?.[0] || "";
|
|
const y = panel.scrollTop + tabsH + 18;
|
|
for (const [key, el] of entries) {
|
|
if (el.offsetTop <= y) active = key;
|
|
}
|
|
setActivePanelTab(active);
|
|
}, { passive: true });
|
|
|
|
syncToolSizeBadges();
|
|
setActivePanelTab(ui.selectedCard?.classList.contains("hidden") ? "tools" : "selected");
|
|
|
|
function setCollapsibleCardState(card, collapsed) {
|
|
markCollapsibleCardState(card, collapsed);
|
|
}
|
|
|
|
function applyDesktopInitialPanelState() {
|
|
const touchFirst = Boolean(window.TarinaiInputMode.classify().touchFirst || document.body.classList.contains("touch-mobile-ui"));
|
|
const desktop = !touchFirst && isDesktopLayout();
|
|
if (!desktop || uiCache.desktopInitialPanelStateApplied) return;
|
|
uiCache.desktopInitialPanelStateApplied = true;
|
|
for (const card of document.querySelectorAll(".collapsible-card")) markCollapsibleCardState(card, false);
|
|
for (const category of ui.toolPalette?.querySelectorAll(".tool-category") || []) markToolCategoryState(category, false);
|
|
}
|
|
|
|
function applyMobileInitialPanelState() {
|
|
const touchFirst = Boolean(window.TarinaiInputMode.classify().touchFirst || document.body.classList.contains("touch-mobile-ui"));
|
|
if (!touchFirst || uiCache.mobileInitialPanelStateApplied) return;
|
|
uiCache.mobileInitialPanelStateApplied = true;
|
|
setCollapsibleCardState(document.querySelector(".colony-card"), true);
|
|
setCollapsibleCardState(document.querySelector(".log-card"), true);
|
|
for (const category of ui.toolPalette?.querySelectorAll(".tool-category") || []) {
|
|
const keepOpen = category.dataset.toolCategory === "operate";
|
|
markToolCategoryState(category, !keepOpen);
|
|
}
|
|
}
|
|
applyDesktopInitialPanelState();
|
|
applyMobileInitialPanelState();
|
|
window.addEventListener("resize", () => { applyDesktopInitialPanelState(); applyMobileInitialPanelState(); }, { passive: true });
|
|
|
|
function updateBackToGameButton() {
|
|
const btn = ui.backToGameBtn;
|
|
if (!btn) return;
|
|
const stage = document.querySelector(".stage-wrap");
|
|
const touchFirst = Boolean(window.TarinaiInputMode.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
|
if (!touchFirst || !stage) { btn.hidden = true; return; }
|
|
const rect = stage.getBoundingClientRect();
|
|
const visible = rect.bottom > 42 && rect.top < Math.min(window.innerHeight * 0.72, window.innerHeight - 90);
|
|
btn.hidden = visible;
|
|
}
|
|
ui.backToGameBtn?.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
audio.uiClick?.();
|
|
scrollPanelToTab("game");
|
|
});
|
|
window.addEventListener("scroll", updateBackToGameButton, { passive: true });
|
|
window.addEventListener("resize", updateBackToGameButton, { passive: true });
|
|
updateBackToGameButton();
|
|
|
|
window.TarinaiEvents.on("selection:changed", () => {
|
|
if (world?._scrollSelectedDataTopOnNextSelection && isDesktopLayout() && world.selected && !world.selected.dead) {
|
|
world._scrollSelectedDataTopOnNextSelection = false;
|
|
window.setTimeout(() => {
|
|
renderSelected?.();
|
|
scrollPanelToTab("selected");
|
|
}, 40);
|
|
return;
|
|
}
|
|
world && (world._scrollSelectedDataTopOnNextSelection = false);
|
|
const touchFirst = Boolean(window.TarinaiInputMode.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
|
const mode = window.TarinaiInputMode.currentMode || uiCache.mobileInputMode || "auto";
|
|
if (!touchFirst || mode !== "auto" || world.tool !== "observe" || !world.selected || world.selected.dead) return;
|
|
window.setTimeout(() => {
|
|
renderSelected?.();
|
|
scrollPanelToTab("selected");
|
|
}, 40);
|
|
});
|
|
|
|
function activeRotatableToolType() {
|
|
const type = toolItemType(world?.tool || "");
|
|
return type && isRotatableItemType(type) ? type : "";
|
|
}
|
|
|
|
function syncMobileRotateControls() {
|
|
const controls = ui.mobileRotateControls;
|
|
if (!controls) return;
|
|
const show = isTouchMobileLayout()
|
|
&& (window.TarinaiInputMode.currentMode || uiCache.mobileInputMode || "auto") !== "camera"
|
|
&& Boolean(activeRotatableToolType());
|
|
controls.hidden = !show;
|
|
}
|
|
|
|
window.syncMobileRotateControls = syncMobileRotateControls;
|
|
|
|
function rotateMobileTool(direction = "right") {
|
|
const type = activeRotatableToolType();
|
|
if (!type || !world?.rotateToolAngle) return false;
|
|
const step = 45 * Math.PI / 180;
|
|
world.rotateToolAngle(type, direction === "left" ? -step : step);
|
|
audio.uiClick?.();
|
|
render();
|
|
syncMobileRotateControls();
|
|
return true;
|
|
}
|
|
|
|
ui.mobileRotateControls?.addEventListener("click", (e) => {
|
|
const btn = e.target.closest?.("[data-mobile-rotate]");
|
|
if (!btn) return;
|
|
e.preventDefault();
|
|
rotateMobileTool(btn.dataset.mobileRotate || "right");
|
|
});
|
|
window.TarinaiEvents.on("tool:selected", syncMobileRotateControls);
|
|
window.addEventListener("resize", syncMobileRotateControls, { passive: true });
|
|
syncMobileRotateControls();
|
|
|
|
window.addEventListener("keydown", (e) => {
|
|
const tag = String(e.target?.tagName || "").toLowerCase();
|
|
if (tag === "input" || tag === "textarea" || e.target?.isContentEditable) return;
|
|
const type = activeRotatableToolType();
|
|
if (!type || !world?.rotateToolAngle) return;
|
|
const key = String(e.key || "").toLowerCase();
|
|
const step = (e.shiftKey ? 5 : 45) * Math.PI / 180;
|
|
if (key === "q" || key === "[") {
|
|
e.preventDefault();
|
|
world.rotateToolAngle(type, -step);
|
|
render();
|
|
syncMobileRotateControls();
|
|
} else if (key === "e" || key === "]") {
|
|
e.preventDefault();
|
|
world.rotateToolAngle(type, step);
|
|
render();
|
|
syncMobileRotateControls();
|
|
} else if (key === "r") {
|
|
e.preventDefault();
|
|
world.setToolAngle?.(type, defaultItemAngle(type));
|
|
render();
|
|
syncMobileRotateControls();
|
|
}
|
|
});
|
|
|
|
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.contains("collapsed");
|
|
markCollapsibleCardState(card, collapsed, { user: true });
|
|
});
|
|
}
|
|
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 = "";
|
|
renderStats();
|
|
});
|
|
const toggleChartSeries = (target) => {
|
|
const icon = target?.closest?.("i[data-series-key]");
|
|
if (!icon) return false;
|
|
const key = String(icon.dataset.seriesKey || "");
|
|
if (!key) return false;
|
|
if (!(uiCache.chartHiddenSeries instanceof Set)) uiCache.chartHiddenSeries = new Set();
|
|
if (uiCache.chartHiddenSeries.has(key)) uiCache.chartHiddenSeries.delete(key);
|
|
else uiCache.chartHiddenSeries.add(key);
|
|
uiCache.lastChartDraw = "";
|
|
audio.uiClick?.();
|
|
renderStats();
|
|
return true;
|
|
};
|
|
ui.colonyChartLegend?.addEventListener("click", (e) => toggleChartSeries(e.target));
|
|
ui.colonyChartLegend?.addEventListener("keydown", (e) => {
|
|
if (e.key !== "Enter" && e.key !== " ") return;
|
|
if (toggleChartSeries(e.target)) e.preventDefault();
|
|
});
|
|
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; }
|
|
window.TarinaiCommands.dispatch(world, { type: "selection.focus", target: t, options: { pulse: 1.15 } });
|
|
selectTool("observe");
|
|
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;
|
|
if (typeof lineageArchiveNeedsRender === "function" && !lineageArchiveNeedsRender()) 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;
|
|
const favResult = window.TarinaiCommands.dispatch(world, { type: "selection.favorite.toggle" });
|
|
if (!favResult?.ok) return;
|
|
uiCache.selectedSnapshot = "";
|
|
renderSelected();
|
|
render();
|
|
showToast(favResult.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", () => {
|
|
window.TarinaiCommands.dispatch(world, { type: "selection.clear" });
|
|
uiCache.showSelectedEmpty = false;
|
|
uiCache.selectedSnapshot = "";
|
|
uiCache.selectedEmpty = false;
|
|
renderSelected();
|
|
render();
|
|
});
|
|
|
|
ui.pauseBtn?.addEventListener("click", () => {
|
|
audio.uiClick?.();
|
|
const pauseResult = window.TarinaiCommands.dispatch(world, { type: "simulation.paused.toggle" });
|
|
const paused = pauseResult?.ok ? pauseResult.paused : world.paused;
|
|
const archiveStale = uiCache.archiveFamilyVersion !== (world.familyVersion || 0);
|
|
if (archiveAutoUpdateEnabled() && paused && archiveStale) {
|
|
scheduleArchiveWindowRender();
|
|
}
|
|
syncTopButtons();
|
|
});
|
|
ui.speedBtn?.addEventListener("click", () => {
|
|
audio.uiClick?.();
|
|
window.TarinaiCommands.dispatch(world, { type: "simulation.speed.cycle", speeds: [1, 2, 4] });
|
|
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 (!isActiveToolMode()) return;
|
|
if (clickedInsideGameOrToolUi(e.target)) return;
|
|
selectTool("observe"); // \u9053\u5177\u8a2d\u7f6e\u30e2\u30fc\u30c9\u3092\u89e3\u9664
|
|
});
|
|
ui.creditsBtn?.addEventListener("click", () => { audio.uiClick?.(); ui.creditsDialog?.classList.remove("hidden"); });
|
|
ui.creditsCloseBtn?.addEventListener("click", () => ui.creditsDialog?.classList.add("hidden"));
|
|
ui.creditsDialog?.addEventListener("click", (e) => {
|
|
if (e.target === ui.creditsDialog) ui.creditsDialog.classList.add("hidden");
|
|
});
|
|
ui.visualSettingsBtn?.addEventListener("click", () => {
|
|
audio.uiClick?.();
|
|
openVisualSettingsDialog();
|
|
});
|
|
ui.visualSettingsCloseBtn?.addEventListener("click", closeVisualSettingsDialog);
|
|
ui.visualSettingsDialog?.addEventListener("click", (e) => {
|
|
if (e.target === ui.visualSettingsDialog) { closeVisualSettingsDialog(); return; }
|
|
const btn = e.target.closest("[data-visual-setting]");
|
|
if (!btn) return;
|
|
const ok = window.TarinaiPerf?.setVisualSetting?.(btn.dataset.visualSetting || "", btn.dataset.visualValue || "");
|
|
if (!ok) return;
|
|
audio.uiClick?.();
|
|
syncVisualSettingsDialog();
|
|
resizeCanvas?.();
|
|
render?.();
|
|
});
|
|
ui.resetBtn?.addEventListener("click", () => {
|
|
audio.uiClick?.();
|
|
openFieldDialog();
|
|
});
|
|
ui.fieldCancelBtn?.addEventListener("click", closeFieldDialog);
|
|
ui.fieldConfirmBtn?.addEventListener("click", () => {
|
|
audio.uiClick?.();
|
|
resetWithField(uiCache.resetFieldType || "garden", uiCache.resetPreset || "default");
|
|
});
|
|
ui.fieldDialog?.addEventListener("click", (e) => {
|
|
if (e.target === ui.fieldDialog) { closeFieldDialog(); return; }
|
|
const fieldBtn = e.target.closest("[data-field-type]");
|
|
if (fieldBtn) {
|
|
audio.uiClick?.();
|
|
selectResetFieldType(fieldBtn.dataset.fieldType || "garden");
|
|
return;
|
|
}
|
|
const presetBtn = e.target.closest("[data-reset-preset]");
|
|
if (presetBtn) {
|
|
audio.uiClick?.();
|
|
selectResetPreset(presetBtn.dataset.resetPreset || "default");
|
|
}
|
|
});
|
|
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");
|
|
});
|
|
|
|
const TOOL_SHORTCUTS = window.TarinaiItemToolMetadata?.TOOL_SHORTCUTS || {};
|
|
const TOOL_BY_SHORTCUT = Object.freeze(Object.fromEntries(Object.entries(TOOL_SHORTCUTS).map(([tool, key]) => [String(key), tool])));
|
|
const isKeyboardTypingTarget = (target) => {
|
|
const tag = String(target?.tagName || "").toLowerCase();
|
|
return tag === "input" || tag === "textarea" || tag === "select" || Boolean(target?.isContentEditable);
|
|
};
|
|
function activateControlTool(tool) {
|
|
if (!tool || !world) return false;
|
|
audio.uiClick?.();
|
|
if (tool === "undo" || tool === "redo") {
|
|
const result = window.TarinaiCommands.dispatch(world, { type: tool === "undo" ? "history.undo" : "history.redo" });
|
|
showToast(result?.ok ? (tool === "undo" ? "Undo" : "Redo") : (tool === "undo" ? "Nothing to undo" : "Nothing to redo"));
|
|
renderStats();
|
|
return true;
|
|
}
|
|
if (tool === "clear_tool") {
|
|
world.pendingLinkEndpoint = null;
|
|
world.copyBuffer = null;
|
|
selectTool("observe");
|
|
for (const b of uiCache.toolButtons || []) b.classList.remove("selected");
|
|
showToast("\u9053\u5177\u9078\u629E\u3092\u89E3\u9664\u3057\u307E\u3057\u305F\u3002");
|
|
return true;
|
|
}
|
|
if (world.tool === tool && cycleToolSize(tool)) return true;
|
|
selectTool(tool);
|
|
return true;
|
|
}
|
|
|
|
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?.();
|
|
const collapsed = !category.classList.contains("collapsed");
|
|
markToolCategoryState(category, collapsed, { user: true });
|
|
return;
|
|
}
|
|
const btn = e.target.closest(".tool");
|
|
if (!btn) return;
|
|
if (btn.dataset.suppressNextClick === "1") {
|
|
btn.dataset.suppressNextClick = "";
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
activateControlTool(btn.dataset.tool);
|
|
});
|
|
|
|
window.addEventListener("keydown", (e) => {
|
|
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey || e.isComposing) return;
|
|
if (isKeyboardTypingTarget(e.target)) return;
|
|
const tool = TOOL_BY_SHORTCUT[String(e.key || "")];
|
|
if (!tool) return;
|
|
e.preventDefault();
|
|
activateControlTool(tool);
|
|
});
|
|
|
|
const normalizedWheelDeltaY = (e) => {
|
|
const line = 18;
|
|
const page = Math.max(160, window.innerHeight * 0.82);
|
|
return e.deltaY * (e.deltaMode === 1 ? line : (e.deltaMode === 2 ? page : 1));
|
|
};
|
|
const applyScrollDelta = (el, dy) => {
|
|
if (!el || !Number.isFinite(dy) || Math.abs(dy) < 0.001) return dy;
|
|
const max = Math.max(0, el.scrollHeight - el.clientHeight);
|
|
const before = clamp(el.scrollTop || 0, 0, max);
|
|
const next = clamp(before + dy, 0, max);
|
|
el.scrollTop = next;
|
|
return dy - (next - before);
|
|
};
|
|
const chainScroll = (el) => {
|
|
if (!el) return;
|
|
el.addEventListener("wheel", (e) => {
|
|
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
|
const dy = normalizedWheelDeltaY(e);
|
|
const max = Math.max(0, el.scrollHeight - el.clientHeight);
|
|
const atTop = el.scrollTop <= 0.5;
|
|
const atBottom = el.scrollTop >= max - 0.5;
|
|
if (!((dy < 0 && atTop) || (dy > 0 && atBottom))) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
applyScrollDelta(document.scrollingElement || document.documentElement, dy);
|
|
}, { passive: false });
|
|
};
|
|
const transferEdgeScroll = (child, parent) => {
|
|
if (!child || !parent) return;
|
|
child.addEventListener("wheel", (e) => {
|
|
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
|
const dy = normalizedWheelDeltaY(e);
|
|
const childCanScroll = child.scrollHeight > child.clientHeight + 1;
|
|
const childMax = Math.max(0, child.scrollHeight - child.clientHeight);
|
|
const movingInside = childCanScroll && ((dy < 0 && child.scrollTop > 0.5) || (dy > 0 && child.scrollTop < childMax - 0.5));
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
let rest = dy;
|
|
if (movingInside) rest = applyScrollDelta(child, dy);
|
|
if (Math.abs(rest) > 0.001) {
|
|
const parentRest = applyScrollDelta(parent, rest);
|
|
if (Math.abs(parentRest) > 0.001) applyScrollDelta(document.scrollingElement || document.documentElement, parentRest);
|
|
}
|
|
}, { passive: false });
|
|
};
|
|
const rightPanel = document.querySelector(".panel");
|
|
chainScroll(rightPanel);
|
|
chainScroll(ui.archiveContent);
|
|
transferEdgeScroll(ui.log, rightPanel);
|
|
|
|
const inputContext = window.TarinaiUIInputShared.createInputContext({ canvas, world, ui, uiCache });
|
|
if (!inputContext) throw new Error("Tarinai UI input context is not available");
|
|
window.TarinaiTouchInput.bindTouchInput(inputContext);
|
|
window.TarinaiMouseInput.bindMouseInput(inputContext);
|
|
|
|
window.addEventListener("keydown", (e) => {
|
|
if (e.key !== "Escape") return;
|
|
closeFieldDialog();
|
|
closeEcologyDialog();
|
|
closeVisualSettingsDialog();
|
|
closeArchiveResetDialog();
|
|
ui.creditsDialog?.classList.add("hidden");
|
|
});
|
|
|
|
window.addEventListener("resize", resizeCanvas);
|
|
}
|