tarinai/js/ui_input_shared.js
2026-06-26 12:46:32 +09:00

394 lines
15 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" || tool === "area_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;
target.playerHeld = true;
target._heldByPlayer = true;
target._playerGrabStartedAt = world.time || 0;
if (target.type === "duplicator") {
target.duplicatorLoadSuppressedUntil = Math.max(target.duplicatorLoadSuppressedUntil || 0, (world.time || 0) + 1.0);
target._duplicatorCandidateKey = "";
target._duplicatorCandidateSince = 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;
}
target.playerHeld = false;
target._heldByPlayer = false;
target._lastPlayerReleasedAt = world.time || 0;
if (target.type === "duplicator") {
target.duplicatorLoadSuppressedUntil = Math.max(target.duplicatorLoadSuppressedUntil || 0, (world.time || 0) + 1.2);
target._duplicatorCandidateKey = "";
target._duplicatorCandidateSince = 0;
}
}
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);