tarinai/js/world_tool_actions.js
2026-06-26 22:35:26 +09:00

237 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
(function (global) {
const World = global.World;
if (!World) throw new Error("World is not available for mixin: world_tool_actions.js");
function forceImmediateToolVisualRefresh(worldRef, reason = "tool-action") {
if (!worldRef) return;
worldRef.drawListDirty = true;
if (worldRef._renderStack) worldRef._renderStack.signature = "";
if (worldRef._visibleRenderStack) {
worldRef._visibleRenderStack.backItems = [];
worldRef._visibleRenderStack.layered = [];
worldRef._visibleRenderStack.carriedPlushies = [];
worldRef._visibleRenderStack.lodgedPins = [];
}
worldRef.markItemBucketsDirty?.(reason);
worldRef.markSpatialDirty?.(reason);
worldRef.ensureSpatial?.(reason);
if (typeof global.render === "function") {
global.render();
global.requestAnimationFrame?.(() => global.render?.());
}
}
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
findDeleteToolTargetAt(x, y) {
let best = null;
let bestD = Infinity;
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
const it = this.items[i];
if (!it || it.dead) continue;
let d = distXY(x, y, it.x, it.y);
if (it.type === "rope" || it.type === "rod") {
const runtime = global.TarinaiLinkRuntime;
const a = runtime?.endpointWorld?.(it.linkA, this);
const b = runtime?.endpointWorld?.(it.linkB, this);
d = runtime?.pointSegmentDistance?.(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y) ?? d;
if (d < 18 && d < bestD) { best = it; bestD = d; }
continue;
}
const result = global.TarinaiCollisionFootprints?.hitTestItem?.(this, it, x, y, { padding: this.screenSizeToWorld ? this.screenSizeToWorld(12) : 12, radiusMultiplier: 2.0 }) || { hit: false, distance: d };
if (result.hit && result.distance < bestD) { best = it; bestD = result.distance; }
}
return best;
},
deleteItemAt(x, y) {
const target = this.findDeleteToolTargetAt?.(x, y);
if (!target) { showToast("消せるものがありません。"); return true; }
if (isPinType(target.type) && target.pinState === "lodged") { showToast("刺さった鋲は、つまんで抜いてください。"); return true; }
const label = toolLabel(target.type);
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, target, {
reason: "delete-tool",
userReason: target.type === "grass_bed" ? "ベッドがなくなった" : "置きものがなくなった",
wake: true,
});
if (!removed) return true;
audio.delete?.();
this.compactItems?.();
this.updateItemCounts?.();
this.rebuildSpatial?.();
this.log?.(`${label}を消した。`, "observe");
return true;
},
areaDeleteRadius() {
return 138;
},
deleteItemsInArea(x, y, options = {}) {
const radius = this.areaDeleteRadius?.() || 138;
const candidates = [];
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
const it = this.items[i];
if (!it || it.dead) continue;
if (isPinType(it.type) && it.pinState === "lodged") continue;
const hitR = Math.max(8, it.r || itemRadiusFor?.(it.type, 12) || 12);
let d = distXY(x, y, it.x, it.y);
if (it.type === "rope" || it.type === "rod") {
const runtime = global.TarinaiLinkRuntime;
const a = runtime?.endpointWorld?.(it.linkA, this);
const b = runtime?.endpointWorld?.(it.linkB, this);
d = runtime?.pointSegmentDistance?.(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y) ?? d;
if (d <= radius + 8) candidates.push(it);
continue;
}
const result = global.TarinaiCollisionFootprints?.hitTestItem?.(this, it, x, y, { padding: radius, radiusMultiplier: 0.85 }) || { hit: d <= radius + hitR * 0.85 };
if (result.hit || d <= radius + hitR * 0.85) candidates.push(it);
}
if (!candidates.length) { if (!options.silentEmpty) showToast("範囲内に消せるものがありません。"); return true; }
let removedCount = 0;
const kindCounts = new Map();
for (const target of candidates) {
if (!target || target.dead) continue;
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, target, {
reason: "area-delete-tool",
userReason: target.type === "grass_bed" ? "ベッドがなくなった" : "置きものがなくなった",
wake: true,
});
if (!removed) continue;
removedCount += 1;
const label = toolLabel(target.type);
kindCounts.set(label, (kindCounts.get(label) || 0) + 1);
}
if (!removedCount) { if (!options.silentEmpty) showToast("範囲内に消せるものがありません。"); return true; }
audio.delete?.();
this.compactItems?.();
this.updateItemCounts?.();
this.rebuildSpatial?.();
this.drawListDirty = true;
if (typeof global.render === "function") global.render();
if (options.drag) this._areaDeleteDragRemoved = (this._areaDeleteDragRemoved || 0) + removedCount;
if (!options.silentLog) {
const detail = [...kindCounts.entries()].slice(0, 4).map(([label, count]) => `${label}${count > 1 ? `×${count}` : ""}`).join("、");
this.log?.(`範囲削除で${removedCount}個消した${detail ? `${detail}` : ""}`, "observe");
showToast(`${removedCount}個消しました。`);
}
return true;
},
linkPointLocalForItem(item, x, y, useClickPoint = false) {
const angle = typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item?.angle) || 0);
const dx = (Number(x || 0) || 0) - (Number(item?.x || 0) || 0);
const dy = (Number(y || 0) || 0) - (Number(item?.y || 0) || 0);
if (!useClickPoint) return { localX: 0, localY: 0 };
const c = Math.cos(-angle), s = Math.sin(-angle);
return { localX: dx * c - dy * s, localY: dx * s + dy * c };
},
findLinkEndpointAt(x, y) {
let best = null;
let bestD = Infinity;
for (let i = (this.tarinai || []).length - 1; i >= 0; i -= 1) {
const t = this.tarinai[i];
if (!t || t.dead || this.isTarinaiHiddenInNestBox?.(t)) continue;
const hit = Math.max(18, (t.radius || 18) * 1.12);
const d = distXY(x, y, t.x, t.y);
if (d <= hit && d < bestD) {
best = { kind: "tarinai", id: t.id, type: t.type || "tarinai", label: t.name || "たりない", x: t.x, y: t.y, localX: 0, localY: 0, center: true };
bestD = d;
}
}
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
const it = this.items[i];
if (!it || it.dead) continue;
if ((this.tool === "rope" || this.tool === "rod") && (it.type === "water" || it.type === "grass_bed")) continue;
const isMechanism = Boolean(global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type));
const isFence = this.isFenceType?.(it.type) || false;
const isConstraint = it.type === "rope" || it.type === "rod";
let hit = false;
let d = distXY(x, y, it.x, it.y);
if (isConstraint) {
const runtime = global.TarinaiLinkRuntime;
const a = runtime?.endpointWorld?.(it.linkA, this);
const b = runtime?.endpointWorld?.(it.linkB, this);
if (!a || !b) continue;
d = runtime?.pointSegmentDistance?.(x, y, a.x, a.y, b.x, b.y) ?? d;
if (d > 20 || d >= bestD) continue;
const vx = b.x - a.x, vy = b.y - a.y;
const len2 = vx * vx + vy * vy || 1;
const attachT = Math.max(0, Math.min(1, ((x - a.x) * vx + (y - a.y) * vy) / len2));
const px = a.x + vx * attachT;
const py = a.y + vy * attachT;
best = { kind: "item", id: it.id, type: it.type, label: toolLabel(it.type), x: px, y: py, localX: 0, localY: 0, center: false, attachT };
bestD = d;
continue;
}
if (isMechanism || isFence) {
const result = global.TarinaiCollisionFootprints?.hitTestItem?.(this, it, x, y, { padding: 18, radiusMultiplier: 1.28 }) || { hit: false, distance: d };
hit = Boolean(result.hit);
d = Math.min(d, result.distance ?? d);
} else {
const result = global.TarinaiCollisionFootprints?.hitTestItem?.(this, it, x, y, { radiusMultiplier: 1.35 }) || { hit: false, distance: d };
hit = Boolean(result.hit);
d = Math.min(d, result.distance ?? d);
}
if (!hit || d >= bestD) continue;
const local = this.linkPointLocalForItem?.(it, x, y, isMechanism || isFence) || { localX: 0, localY: 0 };
best = { kind: "item", id: it.id, type: it.type, label: toolLabel(it.type), x: isMechanism || isFence ? x : it.x, y: isMechanism || isFence ? y : it.y, localX: local.localX, localY: local.localY, center: !(isMechanism || isFence) };
if (isMechanism) global.TarinaiLinkRuntime?.snapEndpointToTarget?.(best, this);
bestD = d;
}
return best;
},
useLinkToolAt(x, y) {
const endpoint = this.findLinkEndpointAt?.(x, y);
const toolName = this.tool === "rod" ? "棒" : "ヒモ";
if (!endpoint) { showToast(`${toolName}をつなぐ対象がありません。`); return true; }
if (!this.pendingLinkEndpoint) {
this.pendingLinkEndpoint = endpoint;
showToast(`${endpoint.label || "対象"}${toolName}の片端をつなぎました。もう片端を選んでください。`);
this.drawListDirty = true;
if (typeof global.render === "function") global.render();
return true;
}
const first = this.pendingLinkEndpoint;
global.TarinaiLinkRuntime?.snapEndpointToTarget?.(first, this);
global.TarinaiLinkRuntime?.snapEndpointToTarget?.(endpoint, this);
if (first.kind === endpoint.kind && first.id === endpoint.id && distXY(first.x, first.y, endpoint.x, endpoint.y) < 8) {
showToast("別の位置または対象を選んでください。");
return true;
}
const runtime = global.TarinaiLinkRuntime;
const p1 = runtime?.endpointWorld?.(first, this) || { x: first.x, y: first.y };
const p2 = runtime?.endpointWorld?.(endpoint, this) || { x: endpoint.x, y: endpoint.y };
const constraintType = this.tool === "rod" ? "rod" : "rope";
const item = new Item(constraintType, (p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);
item.world = this;
item.linkA = { ...first, x: p1.x, y: p1.y };
item.linkB = { ...endpoint, x: p2.x, y: p2.y };
item.linkLength = Math.max(24, distXY(p1.x, p1.y, p2.x, p2.y));
item.linkMidX = (p1.x + p2.x) * 0.5;
item.linkMidY = (p1.y + p2.y) * 0.5;
item.linkMidVX = 0;
item.linkMidVY = 0;
item.r = Math.max(18, item.linkLength * 0.5);
item.amount = 999;
const placed = this.addItem?.(item, constraintType);
this.pendingLinkEndpoint = null;
if (!placed) { showToast(`${toolName}を追加できませんでした。`); return true; }
global.TarinaiPhysicsSoftClear?.clearForItem?.(this, item, `${constraintType}-soft-overlap-clear`);
forceImmediateToolVisualRefresh(this, `${constraintType}-create`);
this.log?.(`${toolName}をつないだ。`, "observe");
showToast(`${toolName}をつなぎました。`);
return true;
},
useToolAt(x, y) {
if (this.tool === "delete") return this.deleteItemAt(x, y);
if (this.tool === "area_delete") return this.deleteItemsInArea(x, y);
if (this.tool === "rope" || this.tool === "rod") return this.useLinkToolAt(x, y);
return false;
},
}));
})(window);