378 lines
14 KiB
JavaScript
378 lines
14 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
function cursorForTool(tool) {
|
|
if (tool === "water_hose") return "crosshair";
|
|
if (tool === "pinch") return "grab";
|
|
if (tool === "observe") return "default";
|
|
if (tool === "poke" || tool === "delete") return "pointer";
|
|
return "crosshair";
|
|
}
|
|
|
|
function createInputContext({ canvas, world, ui, uiCache }) {
|
|
if (!canvas || !world || !uiCache) return null;
|
|
const inputModeManager = global.TarinaiInputMode;
|
|
|
|
function 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");
|
|
return mode;
|
|
}
|
|
|
|
function updateTouchMobileUiClass() {
|
|
const enabled = Boolean(inputModeManager?.refresh?.().touchFirst);
|
|
document.body.classList.toggle("touch-mobile-ui", enabled);
|
|
if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled;
|
|
return enabled;
|
|
}
|
|
|
|
function mobileInputMode() {
|
|
return inputModeManager?.currentMode || uiCache.mobileInputMode || "auto";
|
|
}
|
|
|
|
function bindMobileModeControls() {
|
|
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}`);
|
|
});
|
|
}
|
|
|
|
function touchPoint(touches, index = 0) {
|
|
const t = touches?.[index];
|
|
if (!t) return null;
|
|
return { clientX: t.clientX, clientY: t.clientY };
|
|
}
|
|
|
|
function 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 };
|
|
}
|
|
|
|
function 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);
|
|
}
|
|
|
|
function 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 };
|
|
}
|
|
|
|
function screenToWorldEvent(e) {
|
|
if (typeof screenToWorld === "function") return screenToWorld(e);
|
|
return screenToWorldClient(e.clientX, e.clientY);
|
|
}
|
|
|
|
function 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 };
|
|
}
|
|
|
|
function 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) && global.TarinaiFreezeSystem?.freezeTarinai) {
|
|
global.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;
|
|
}
|
|
|
|
function prepareGrabTarget(target) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
function beginGrab(p, clientX, clientY, options = {}) {
|
|
const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null;
|
|
if (!target) {
|
|
if (options.notifyNoTarget) showToast("\u3064\u307e\u3081\u308b\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002");
|
|
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 = clientX || 0;
|
|
uiCache.grabStartY = clientY || 0;
|
|
prepareGrabTarget(target);
|
|
if (options.touchMode) uiCache.touchMode = options.touchMode;
|
|
renderStats();
|
|
return true;
|
|
}
|
|
|
|
function moveGrab(p, clientX, clientY, options = {}) {
|
|
const target = uiCache.grabTarget;
|
|
if (!target || target.dead) {
|
|
uiCache.grabbing = false;
|
|
uiCache.grabTarget = null;
|
|
if (options.touch) uiCache.touchMode = "";
|
|
canvas.style.cursor = world.tool === "pinch" ? "grab" : "default";
|
|
return false;
|
|
}
|
|
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 pad = target.radius || target.r || 16;
|
|
if (uiCache.grabKind === "tarinai" && !p.inside) {
|
|
const raw = clientToWorldRaw(clientX, 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");
|
|
if (options.touch) 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;
|
|
}
|
|
canvas.style.cursor = "grabbing";
|
|
if (options.renderFrame) render();
|
|
return true;
|
|
}
|
|
|
|
function endGrab(clientX, clientY) {
|
|
if (!uiCache.grabbing) return false;
|
|
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, clientX, 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 = cursorForTool(world.tool);
|
|
renderStats();
|
|
return true;
|
|
}
|
|
|
|
function beginHose(p, clientX, clientY, options = {}) {
|
|
audio.waterHose?.();
|
|
uiCache.hosing = true;
|
|
uiCache.hoseMoved = false;
|
|
uiCache.hoseLastWorldX = p.x;
|
|
uiCache.hoseLastWorldY = p.y;
|
|
uiCache.hoseStartX = clientX;
|
|
uiCache.hoseStartY = clientY;
|
|
world.applyWaterHose?.(p.x, p.y, 0, 0, 0.10);
|
|
if (options.touchMode) uiCache.touchMode = options.touchMode;
|
|
render();
|
|
return true;
|
|
}
|
|
|
|
function moveHose(p, clientX, clientY, options = {}) {
|
|
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(clientX - (uiCache.hoseStartX || clientX), clientY - (uiCache.hoseStartY || clientY));
|
|
if (totalMove > 3) {
|
|
uiCache.hoseMoved = true;
|
|
if (options.touch) uiCache.touchMoved = true;
|
|
}
|
|
if (p.inside) world.applyWaterHose?.(p.x, p.y, dx, dy, 0.10);
|
|
canvas.style.cursor = "crosshair";
|
|
render();
|
|
}
|
|
|
|
function endHose() {
|
|
if (!uiCache.hosing) return false;
|
|
uiCache.hosing = false;
|
|
canvas.style.cursor = cursorForTool(world.tool);
|
|
world.rebuildSpatial?.();
|
|
renderStats();
|
|
return true;
|
|
}
|
|
|
|
function beginPan(clientX, clientY, button = null) {
|
|
uiCache.panning = true;
|
|
uiCache.panMoved = false;
|
|
uiCache.panButton = button;
|
|
uiCache.panStartX = clientX;
|
|
uiCache.panStartY = clientY;
|
|
uiCache.panLastX = clientX;
|
|
uiCache.panLastY = clientY;
|
|
}
|
|
|
|
function movePan(clientX, clientY) {
|
|
if (!uiCache.panning) return false;
|
|
const dx = clientX - uiCache.panLastX;
|
|
const dy = clientY - uiCache.panLastY;
|
|
uiCache.panLastX = clientX;
|
|
uiCache.panLastY = clientY;
|
|
const totalMove = Math.hypot(clientX - uiCache.panStartX, clientY - uiCache.panStartY);
|
|
if (totalMove > 8) {
|
|
canvas.style.cursor = "grabbing";
|
|
world.panCamera?.(-dx, -dy);
|
|
uiCache.panMoved = true;
|
|
render();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function endPan(button = null) {
|
|
if (!uiCache.panning) return false;
|
|
if (uiCache.panButton !== null && button !== null && button !== uiCache.panButton) return false;
|
|
uiCache.panning = false;
|
|
uiCache.panButton = null;
|
|
canvas.style.cursor = cursorForTool(world.tool);
|
|
return true;
|
|
}
|
|
|
|
function updatePointerFromEvent(e) {
|
|
const p = screenToWorldEvent(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();
|
|
}
|
|
|
|
function cancelPointerActions() {
|
|
uiCache.touchMode = "";
|
|
uiCache.touchMoved = false;
|
|
uiCache.hosing = false;
|
|
uiCache.grabbing = false;
|
|
uiCache.grabTarget = null;
|
|
uiCache.grabKind = "";
|
|
}
|
|
|
|
return {
|
|
canvas,
|
|
world,
|
|
ui,
|
|
uiCache,
|
|
cursorForTool,
|
|
setMobileInputMode,
|
|
updateTouchMobileUiClass,
|
|
mobileInputMode,
|
|
bindMobileModeControls,
|
|
touchPoint,
|
|
touchCenter,
|
|
touchDistance,
|
|
screenToWorldClient,
|
|
screenToWorldEvent,
|
|
clientToWorldRaw,
|
|
setZoomAroundClientPoint,
|
|
beginGrab,
|
|
moveGrab,
|
|
endGrab,
|
|
beginHose,
|
|
moveHose,
|
|
endHose,
|
|
beginPan,
|
|
movePan,
|
|
endPan,
|
|
updatePointerFromEvent,
|
|
cancelPointerActions,
|
|
};
|
|
}
|
|
|
|
global.TarinaiUIInputShared = Object.freeze({ createInputContext, cursorForTool });
|
|
})(window);
|