237 lines
12 KiB
JavaScript
237 lines
12 KiB
JavaScript
"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);
|