640 lines
26 KiB
JavaScript
640 lines
26 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
function cursorForTool(tool) {
|
|
if (tool === "water_hose") return "crosshair";
|
|
if (tool === "shoot") return "none";
|
|
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"].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-auto", mode === "auto");
|
|
document.body.classList.toggle("mobile-mode-camera", mode === "camera");
|
|
document.body.classList.toggle("mobile-mode-tool", mode === "tool");
|
|
global.syncMobileRotateControls?.();
|
|
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;
|
|
global.syncMobileRotateControls?.();
|
|
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");
|
|
const root = ui.mobileModeControls;
|
|
if (!root || root.dataset.bound === "1") return;
|
|
root.dataset.bound = "1";
|
|
const activate = (e) => {
|
|
const btn = e.target?.closest?.("[data-mobile-mode]");
|
|
if (!btn || !root.contains(btn)) return;
|
|
const now = performance?.now?.() ?? Date.now();
|
|
if (now - Number(root.dataset.lastActivateAt || 0) < 180) return;
|
|
root.dataset.lastActivateAt = String(now);
|
|
e.preventDefault?.();
|
|
e.stopPropagation?.();
|
|
window.TarinaiTooltips.hide();
|
|
audio.uiClick?.();
|
|
setMobileInputMode(btn.dataset.mobileMode || "auto");
|
|
showToast(`\u30b9\u30de\u30db\u64cd\u4f5c: ${btn.textContent || btn.dataset.mobileMode}`);
|
|
};
|
|
root.addEventListener("pointerup", activate, { passive: false });
|
|
root.addEventListener("touchend", activate, { passive: false });
|
|
root.addEventListener("click", activate);
|
|
}
|
|
|
|
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) {
|
|
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 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;
|
|
|
|
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 syncGrabbedPhysicsItem(target, reason = "pinch-move") {
|
|
if (!target || target.dead) return false;
|
|
const pb = global.TarinaiPhysicsBodySystem;
|
|
const isBody = typeof pb?.isBodyType === "function"
|
|
? pb.isBodyType(target)
|
|
: Boolean(typeof pb?.isPhysicsType === "function" && pb.isPhysicsType(target) && typeof pb?.ensureBody === "function" && pb.ensureBody(target, null, { syncFromLegacy: false }));
|
|
if (!isBody) return false;
|
|
pb.syncPoseFromItem(target);
|
|
pb.setScalar(target, "awakeUntil", Math.max(Number(pb.scalar(target, "awakeUntil", 0) || 0), (world.time || 0) + 0.65), reason);
|
|
global.TarinaiMechanicalSystem.invalidateGeometry(target);
|
|
return true;
|
|
}
|
|
|
|
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) global.TarinaiItemDynamicPinSystem.detach(lodged, 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") global.TarinaiItemDynamicPinSystem.detach(target, 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;
|
|
}
|
|
global.TarinaiHistory.capture(world, "pinch");
|
|
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.grabVelocityX = 0;
|
|
uiCache.grabVelocityY = 0;
|
|
uiCache.grabLastMoveAt = performance?.now?.() ?? Date.now();
|
|
uiCache.grabStartX = clientX || 0;
|
|
uiCache.grabStartY = clientY || 0;
|
|
target.playerHeld = true;
|
|
target._heldByPlayer = true;
|
|
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-start");
|
|
if (target.type === "duplicator") {
|
|
target.duplicatorLoadSuppressedUntil = Math.max(target.duplicatorLoadSuppressedUntil || 0, (world.time || 0) + 1.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);
|
|
const moveNow = performance?.now?.() ?? Date.now();
|
|
const moveDt = Math.max(0.012, Math.min(0.18, (moveNow - (uiCache.grabLastMoveAt || moveNow)) / 1000));
|
|
uiCache.grabLastMoveAt = moveNow;
|
|
uiCache.grabVelocityX = clamp(dx / moveDt, -820, 820);
|
|
uiCache.grabVelocityY = clamp(dy / moveDt, -820, 820);
|
|
uiCache.grabLastWorldX = p.x;
|
|
uiCache.grabLastWorldY = p.y;
|
|
const beforeX = Number(target.x || 0) || 0;
|
|
const beforeY = Number(target.y || 0) || 0;
|
|
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);
|
|
}
|
|
const movedX = (Number(target.x || 0) || 0) - beforeX;
|
|
const movedY = (Number(target.y || 0) || 0) - beforeY;
|
|
if (uiCache.grabKind === "item" && target.type === "reciprocator") {
|
|
const pb = (typeof window !== "undefined" ? window : globalThis).TarinaiPhysicsBodySystem;
|
|
pb?.setScalar?.(target, "railAnchorX", (Number(pb?.scalar?.(target, "railAnchorX", beforeX)) || beforeX) + movedX, "grab-move");
|
|
pb?.setScalar?.(target, "railAnchorY", (Number(pb?.scalar?.(target, "railAnchorY", beforeY)) || beforeY) + movedY, "grab-move");
|
|
target.prevX = target.x;
|
|
target.prevY = target.y;
|
|
}
|
|
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-move");
|
|
if (uiCache.grabKind === "tarinai") {
|
|
// While a Tarinai is held, its AI/passive physics must not keep the last
|
|
// drag velocity. Otherwise the body starts sliding when the cursor is
|
|
// stopped but the button is still down.
|
|
target.vx = 0;
|
|
target.vy = 0;
|
|
target.impulseVx = 0;
|
|
target.impulseVy = 0;
|
|
target._lastHeldByPlayerAt = world.time || 0;
|
|
target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
|
if (options.touch) target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
|
} else {
|
|
target.vx = clamp(uiCache.grabVelocityX || dx * 16, -820, 820);
|
|
target.vy = clamp(uiCache.grabVelocityY || dy * 16, -820, 820);
|
|
}
|
|
if ((target.type === "ball" || target.type === "balloon")) {
|
|
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 (uiCache.grabKind === "item") {
|
|
const releaseVx = clamp(Number(uiCache.grabVelocityX || target.vx || 0) || 0, -900, 900);
|
|
const releaseVy = clamp(Number(uiCache.grabVelocityY || target.vy || 0) || 0, -900, 900);
|
|
const releaseSpeed = Math.hypot(releaseVx, releaseVy);
|
|
if (target.type === "plushie" && typeof target.fling === "function") {
|
|
const sourceX = Number(uiCache.grabLastWorldX ?? target.x) || target.x;
|
|
const sourceY = Number(uiCache.grabLastWorldY ?? target.y) || target.y;
|
|
const vx = releaseSpeed > 80 ? releaseVx : 0;
|
|
const vy = releaseSpeed > 80 ? releaseVy : 0;
|
|
target.fling(world, sourceX, sourceY, { source: "pinch", vx, vy, speed: Math.max(620, releaseSpeed * 1.05), life: 2.8 });
|
|
} else if (releaseSpeed > 18) {
|
|
target.vx = releaseVx;
|
|
target.vy = releaseVy;
|
|
const frameDt = Math.max(1 / 90, Math.min(1 / 30, Number(world.dt || 1 / 60) || 1 / 60));
|
|
target.prevX = target.x - releaseVx * frameDt;
|
|
target.prevY = target.y - releaseVy * frameDt;
|
|
if ((target.type === "ball" || target.type === "balloon")) target.spinVelocity = clamp((target.spinVelocity || 0) + releaseVx * 0.018, -42, 42);
|
|
} else if ((target.type === "ball" || target.type === "balloon")) {
|
|
target.prevX = target.x;
|
|
target.prevY = target.y;
|
|
}
|
|
}
|
|
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-release");
|
|
target.playerHeld = false;
|
|
target._heldByPlayer = false;
|
|
if (uiCache.grabKind === "tarinai") {
|
|
target.vx = 0;
|
|
target.vy = 0;
|
|
target.impulseVx = 0;
|
|
target.impulseVy = 0;
|
|
target.pinchThoughtTimer = 0;
|
|
if (target.thought === "つままれている") target.thought = "";
|
|
const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(target) : null;
|
|
const reason = String(behavior?.reason || behavior?.text || behavior?.label || "");
|
|
if (reason === "つままれている" && typeof clearBehavior === "function") clearBehavior(target);
|
|
}
|
|
if (target.type === "duplicator") {
|
|
target.duplicatorLoadSuppressedUntil = Math.max(target.duplicatorLoadSuppressedUntil || 0, (world.time || 0) + 1.2);
|
|
}
|
|
}
|
|
audio.drop?.();
|
|
uiCache.grabbing = false;
|
|
uiCache.grabTarget = null;
|
|
uiCache.grabKind = "";
|
|
uiCache.grabVelocityX = 0;
|
|
uiCache.grabVelocityY = 0;
|
|
uiCache.grabLastMoveAt = 0;
|
|
world.rebuildSpatial?.();
|
|
uiCache.grabLastSpatialAt = 0;
|
|
canvas.style.cursor = cursorForTool(world.tool);
|
|
renderStats();
|
|
return true;
|
|
}
|
|
|
|
function beginHose(p, clientX, clientY, options = {}) {
|
|
global.TarinaiHistory.capture(world, "water-hose");
|
|
audio.waterHose?.();
|
|
uiCache.hosing = true;
|
|
uiCache.hoseMoved = false;
|
|
uiCache.hoseLastWorldX = p.x;
|
|
uiCache.hoseLastWorldY = p.y;
|
|
uiCache.hoseStartX = clientX;
|
|
uiCache.hoseStartY = clientY;
|
|
uiCache.hoseLastApplyAt = global.performance?.now?.() || Date.now();
|
|
world.applyWaterHose?.(p.x, p.y, 0, 0, 0.18);
|
|
if (options.touchMode) uiCache.touchMode = options.touchMode;
|
|
render();
|
|
return true;
|
|
}
|
|
|
|
function moveHose(p, clientX, clientY, options = {}) {
|
|
uiCache.hoseLastClientX = clientX;
|
|
uiCache.hoseLastClientY = clientY;
|
|
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) {
|
|
const now = global.performance?.now?.() || Date.now();
|
|
const elapsed = Math.max(0.06, Math.min(0.22, (now - (uiCache.hoseLastApplyAt || now)) / 1000));
|
|
uiCache.hoseLastApplyAt = now;
|
|
world.applyWaterHose?.(p.x, p.y, dx, dy, elapsed);
|
|
}
|
|
canvas.style.cursor = "crosshair";
|
|
render();
|
|
}
|
|
|
|
function endHose() {
|
|
if (!uiCache.hosing) return false;
|
|
uiCache.hosing = false;
|
|
uiCache.hoseLastApplyAt = 0;
|
|
canvas.style.cursor = cursorForTool(world.tool);
|
|
world.rebuildSpatial?.();
|
|
renderStats();
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
function beginShoot(p, clientX, clientY, options = {}) {
|
|
if (!p?.inside) return false;
|
|
global.TarinaiHistory.capture(world, "shoot");
|
|
uiCache.shooting = true;
|
|
uiCache.shootSuppressClick = true;
|
|
uiCache.shootFired = false;
|
|
uiCache.shootStartX = clientX;
|
|
uiCache.shootStartY = clientY;
|
|
world.pointer.x = p.x;
|
|
world.pointer.y = p.y;
|
|
world.pointer.inside = true;
|
|
world.setShootCursorFromPointer?.(p);
|
|
uiCache.shootLastRawWorldX = p.x;
|
|
uiCache.shootLastRawWorldY = p.y;
|
|
uiCache.shootFired = Boolean(world.fireShootTool?.()) || Boolean(uiCache.shootFired);
|
|
if (options.touchMode) uiCache.touchMode = options.touchMode;
|
|
if (uiCache.shootHoldTimer) clearInterval(uiCache.shootHoldTimer);
|
|
uiCache.shootHoldTimer = setInterval(() => {
|
|
if (!uiCache.shooting) {
|
|
clearInterval(uiCache.shootHoldTimer);
|
|
uiCache.shootHoldTimer = null;
|
|
return;
|
|
}
|
|
uiCache.shootFired = Boolean(world.fireShootTool?.()) || Boolean(uiCache.shootFired);
|
|
}, 120);
|
|
canvas.style.cursor = "none";
|
|
render();
|
|
return true;
|
|
}
|
|
|
|
function moveShoot(p, clientX, clientY, options = {}) {
|
|
const rawDx = (Number(p.x) || 0) - (Number(uiCache.shootLastRawWorldX ?? p.x) || 0);
|
|
const rawDy = (Number(p.y) || 0) - (Number(uiCache.shootLastRawWorldY ?? p.y) || 0);
|
|
uiCache.shootLastRawWorldX = p.x;
|
|
uiCache.shootLastRawWorldY = p.y;
|
|
world.pointer.x = p.x;
|
|
world.pointer.y = p.y;
|
|
world.pointer.inside = !!p.inside;
|
|
if (p.inside) world.moveShootCursorBy?.(rawDx, rawDy);
|
|
const totalMove = Math.hypot(clientX - (uiCache.shootStartX || clientX), clientY - (uiCache.shootStartY || clientY));
|
|
if (totalMove > 3) {
|
|
if (options.touch) uiCache.touchMoved = true;
|
|
}
|
|
canvas.style.cursor = "none";
|
|
render();
|
|
return true;
|
|
}
|
|
|
|
function endShoot() {
|
|
if (!uiCache.shooting) return false;
|
|
uiCache.shooting = false;
|
|
if (uiCache.shootHoldTimer) {
|
|
clearInterval(uiCache.shootHoldTimer);
|
|
uiCache.shootHoldTimer = null;
|
|
}
|
|
if (uiCache.shootFired) audio.boltAction?.();
|
|
uiCache.shootFired = false;
|
|
canvas.style.cursor = cursorForTool(world.tool);
|
|
renderStats();
|
|
return true;
|
|
}
|
|
|
|
|
|
function beginAreaDelete(p, clientX, clientY, options = {}) {
|
|
global.TarinaiHistory.capture(world, "area-delete");
|
|
uiCache.areaDeleting = true;
|
|
uiCache.areaDeleteMoved = false;
|
|
uiCache.areaDeleteStartX = clientX;
|
|
uiCache.areaDeleteStartY = clientY;
|
|
world._areaDeleteDragRemoved = 0;
|
|
if (p.inside) world.deleteItemsInArea?.(p.x, p.y, { silentEmpty: true, silentLog: true, drag: true });
|
|
if (options.touchMode) uiCache.touchMode = options.touchMode;
|
|
render();
|
|
return true;
|
|
}
|
|
|
|
function moveAreaDelete(p, clientX, clientY, options = {}) {
|
|
const totalMove = Math.hypot(clientX - (uiCache.areaDeleteStartX || clientX), clientY - (uiCache.areaDeleteStartY || clientY));
|
|
if (totalMove > 3) {
|
|
uiCache.areaDeleteMoved = true;
|
|
if (options.touch) uiCache.touchMoved = true;
|
|
}
|
|
if (p.inside) world.deleteItemsInArea?.(p.x, p.y, { silentEmpty: true, silentLog: true, drag: true });
|
|
canvas.style.cursor = "crosshair";
|
|
render();
|
|
return true;
|
|
}
|
|
|
|
function endAreaDelete() {
|
|
if (!uiCache.areaDeleting) return false;
|
|
uiCache.areaDeleting = false;
|
|
const removed = world._areaDeleteDragRemoved || 0;
|
|
world._areaDeleteDragRemoved = 0;
|
|
canvas.style.cursor = cursorForTool(world.tool);
|
|
world.rebuildSpatial?.();
|
|
renderStats();
|
|
if (removed > 0) {
|
|
world.log?.(`\u7bc4\u56f2\u524a\u9664\u3067${removed}\u500b\u6d88\u3057\u305f\u3002`, "observe");
|
|
showToast(`${removed}\u500b\u6d88\u3057\u307e\u3057\u305f\u3002`);
|
|
}
|
|
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);
|
|
if (world.tool === "shoot") {
|
|
if (p.inside) {
|
|
if (world.shootCursorActive) {
|
|
const lastRawX = Number.isFinite(Number(world.shootPointerRawX)) ? Number(world.shootPointerRawX) : prevX;
|
|
const lastRawY = Number.isFinite(Number(world.shootPointerRawY)) ? Number(world.shootPointerRawY) : prevY;
|
|
world.moveShootCursorBy?.((Number(p.x) || 0) - (Number(lastRawX) || 0), (Number(p.y) || 0) - (Number(lastRawY) || 0));
|
|
} else {
|
|
world.setShootCursorFromPointer?.(p);
|
|
}
|
|
world.shootPointerRawX = p.x;
|
|
world.shootPointerRawY = p.y;
|
|
}
|
|
canvas.style.cursor = "none";
|
|
} else if (world.tool === "pinch" && p.inside && !uiCache.grabbing && !uiCache.panning && !uiCache.hosing && !uiCache.areaDeleting && !uiCache.shooting) {
|
|
const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null;
|
|
if (world.hoverGrabTarget !== target) {
|
|
world.hoverGrabTarget = target || null;
|
|
world.drawListDirty = true;
|
|
}
|
|
if (world.hoverDeleteTarget) { world.hoverDeleteTarget = null; world.drawListDirty = true; }
|
|
canvas.style.cursor = target ? "grab" : "default";
|
|
} else if (world.tool === "delete" && p.inside && !uiCache.grabbing && !uiCache.panning && !uiCache.hosing && !uiCache.areaDeleting && !uiCache.shooting) {
|
|
const target = world.findDeleteToolTargetAt ? world.findDeleteToolTargetAt(p.x, p.y) : null;
|
|
if (world.hoverDeleteTarget !== target) {
|
|
world.hoverDeleteTarget = target || null;
|
|
world.drawListDirty = true;
|
|
}
|
|
if (world.hoverGrabTarget) { world.hoverGrabTarget = null; world.drawListDirty = true; }
|
|
canvas.style.cursor = target ? "pointer" : "default";
|
|
} else {
|
|
if (world.hoverGrabTarget) {
|
|
world.hoverGrabTarget = null;
|
|
world.drawListDirty = true;
|
|
}
|
|
if (world.hoverDeleteTarget) {
|
|
world.hoverDeleteTarget = null;
|
|
world.drawListDirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function cancelPointerActions() {
|
|
uiCache.touchMode = "";
|
|
uiCache.touchMoved = false;
|
|
uiCache.hosing = false;
|
|
if (uiCache.hoseHoldTimer) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; }
|
|
uiCache.shooting = false;
|
|
uiCache.shootFired = false;
|
|
if (uiCache.shootHoldTimer) { clearInterval(uiCache.shootHoldTimer); uiCache.shootHoldTimer = null; }
|
|
uiCache.areaDeleting = false;
|
|
uiCache.grabbing = false;
|
|
uiCache.grabTarget = null;
|
|
uiCache.grabKind = "";
|
|
uiCache.grabVelocityX = 0;
|
|
uiCache.grabVelocityY = 0;
|
|
uiCache.grabLastMoveAt = 0;
|
|
if (world) {
|
|
world.hoverGrabTarget = null;
|
|
world.hoverDeleteTarget = null;
|
|
}
|
|
}
|
|
|
|
return {
|
|
canvas,
|
|
world,
|
|
ui,
|
|
uiCache,
|
|
cursorForTool,
|
|
setMobileInputMode,
|
|
updateTouchMobileUiClass,
|
|
mobileInputMode,
|
|
bindMobileModeControls,
|
|
touchPoint,
|
|
touchCenter,
|
|
touchDistance,
|
|
screenToWorldClient,
|
|
screenToWorldEvent,
|
|
clientToWorldRaw,
|
|
setZoomAroundClientPoint,
|
|
beginGrab,
|
|
moveGrab,
|
|
endGrab,
|
|
beginHose,
|
|
moveHose,
|
|
endHose,
|
|
beginShoot,
|
|
moveShoot,
|
|
endShoot,
|
|
beginAreaDelete,
|
|
moveAreaDelete,
|
|
endAreaDelete,
|
|
beginPan,
|
|
movePan,
|
|
endPan,
|
|
updatePointerFromEvent,
|
|
cancelPointerActions,
|
|
};
|
|
}
|
|
|
|
global.TarinaiUIInputShared = Object.freeze({ createInputContext, cursorForTool });
|
|
})(window);
|