491 lines
27 KiB
JavaScript
491 lines
27 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");
|
|
|
|
|
|
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
|
_forceImmediateToolVisualRefresh(reason = "tool-action") {
|
|
this.drawListDirty = true;
|
|
if (this._renderStack) this._renderStack.signature = "";
|
|
if (this._visibleRenderStack) {
|
|
this._visibleRenderStack.backItems = [];
|
|
this._visibleRenderStack.layered = [];
|
|
this._visibleRenderStack.carriedPlushies = [];
|
|
this._visibleRenderStack.lodgedPins = [];
|
|
}
|
|
this.markItemBucketsDirty?.(reason);
|
|
this.markSpatialDirty?.(reason);
|
|
},
|
|
findDeleteToolTargetAt(x, y) {
|
|
let best = null;
|
|
let bestD = Infinity;
|
|
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
|
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
|
const it = itemCandidates[i];
|
|
if (!it || it.dead) continue;
|
|
let d = distXY(x, y, it.x, it.y);
|
|
if (["rope", "rod", "spring", "wire", "insulated_wire"].includes(it.type)) {
|
|
const runtime = global.TarinaiLinkRuntime;
|
|
const pb = global.TarinaiPhysicsBodySystem;
|
|
const a = runtime?.endpointWorld?.(pb?.endpoint?.(it, 0), this);
|
|
const b = runtime?.endpointWorld?.(pb?.endpoint?.(it, 1), this);
|
|
d = global.TarinaiGeometry.pointSegmentDistance(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y);
|
|
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) {
|
|
global.TarinaiAchievements?.recordIntervention?.({ world: this, tool: "delete", x, y });
|
|
const target = this.findDeleteToolTargetAt?.(x, y);
|
|
if (!target) { showToast("\u6d88\u305b\u308b\u3082\u306e\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return true; }
|
|
if (isPinType(target.type) && target.pinState === "lodged") { showToast("\u523a\u3055\u3063\u305f\u92f2\u306f\u3001\u3064\u307e\u3093\u3067\u629c\u3044\u3066\u304f\u3060\u3055\u3044\u3002"); return true; }
|
|
const label = toolLabel(target.type);
|
|
const removed = global.TarinaiStructureLifecycle.deleteItem(this, target, {
|
|
reason: "delete-tool",
|
|
userReason: target.type === "grass_bed" ? "\u30d9\u30c3\u30c9\u304c\u306a\u304f\u306a\u3063\u305f" : "\u7f6e\u304d\u3082\u306e\u304c\u306a\u304f\u306a\u3063\u305f",
|
|
wake: true,
|
|
});
|
|
if (!removed) return true;
|
|
global.TarinaiAchievements?.recordPlayerDeletion?.({ world: this, item: target, tool: "delete" });
|
|
audio.delete?.();
|
|
this.compactItems?.();
|
|
this.updateItemCounts?.();
|
|
this.rebuildSpatial?.();
|
|
this.log?.(`${label}\u3092\u6d88\u3057\u305f\u3002`, "observe");
|
|
return true;
|
|
},
|
|
|
|
areaDeleteRadius() {
|
|
return 138;
|
|
},
|
|
|
|
deleteItemsInArea(x, y, options = {}) {
|
|
global.TarinaiAchievements?.recordIntervention?.({ world: this, tool: "area_delete", x, y });
|
|
const radius = this.areaDeleteRadius?.() || 138;
|
|
const candidates = [];
|
|
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
|
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
|
const it = itemCandidates[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 (["rope", "rod", "spring", "wire", "insulated_wire"].includes(it.type)) {
|
|
const runtime = global.TarinaiLinkRuntime;
|
|
const pb = global.TarinaiPhysicsBodySystem;
|
|
const a = runtime?.endpointWorld?.(pb?.endpoint?.(it, 0), this);
|
|
const b = runtime?.endpointWorld?.(pb?.endpoint?.(it, 1), this);
|
|
d = global.TarinaiGeometry.pointSegmentDistance(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y);
|
|
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("\u7bc4\u56f2\u5185\u306b\u6d88\u305b\u308b\u3082\u306e\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); 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" ? "\u30d9\u30c3\u30c9\u304c\u306a\u304f\u306a\u3063\u305f" : "\u7f6e\u304d\u3082\u306e\u304c\u306a\u304f\u306a\u3063\u305f",
|
|
wake: true,
|
|
});
|
|
if (!removed) continue;
|
|
global.TarinaiAchievements?.recordPlayerDeletion?.({ world: this, item: target, tool: "area_delete" });
|
|
removedCount += 1;
|
|
const label = toolLabel(target.type);
|
|
kindCounts.set(label, (kindCounts.get(label) || 0) + 1);
|
|
}
|
|
if (!removedCount) { if (!options.silentEmpty) showToast("\u7bc4\u56f2\u5185\u306b\u6d88\u305b\u308b\u3082\u306e\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); 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 ? `\u00d7${count}` : ""}`).join("\u3001");
|
|
this.log?.(`\u7bc4\u56f2\u524a\u9664\u3067${removedCount}\u500b\u6d88\u3057\u305f${detail ? `\uff08${detail}\uff09` : ""}\u3002`, "observe");
|
|
showToast(`${removedCount}\u500b\u6d88\u3057\u307e\u3057\u305f\u3002`);
|
|
}
|
|
return true;
|
|
},
|
|
|
|
linkPointLocalForItem(item, x, y, useClickPoint = false) {
|
|
const angle = itemAngleFor(item);
|
|
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;
|
|
const wireTool = this.tool === "wire" || this.tool === "insulated_wire";
|
|
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 || "", liveToken: t.liveToken ?? null, _ref: t, type: t.type || "tarinai", label: t.name || "\u305f\u308a\u306a\u3044", x: t.x, y: t.y, localX: 0, localY: 0, center: true };
|
|
bestD = d;
|
|
}
|
|
}
|
|
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
|
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
|
const it = itemCandidates[i];
|
|
if (!it || it.dead || it.type === "splat") continue;
|
|
if ((this.tool === "rope" || this.tool === "rod" || this.tool === "spring") && global.TarinaiPhysicsSoftClear.isSoftPlacementType(it.type)) continue;
|
|
if (wireTool && ["rope", "rod", "spring", "wire", "insulated_wire"].includes(it.type)) continue;
|
|
if (wireTool && it.type === "circuit_board") {
|
|
const portHit = global.TarinaiCircuitBoardSystem?.nearestPort?.(it, x, y, 30);
|
|
if (!portHit || portHit.distance >= bestD) continue;
|
|
best = { kind: "item", id: it.id, type: it.type, label: `${toolLabel(it.type)} ${portHit.port.label}`, x: portHit.x, y: portHit.y, localX: portHit.localX, localY: portHit.localY, center: false, portId: portHit.port.portId };
|
|
bestD = portHit.distance;
|
|
continue;
|
|
}
|
|
const isMechanism = Boolean(global.TarinaiMechanicalSystem.isMechanicalType(it.type));
|
|
const isFence = this.isFenceType?.(it.type) || false;
|
|
const isConstraint = ["rope", "rod", "spring", "wire", "insulated_wire"].includes(it.type);
|
|
let hit = false;
|
|
let d = distXY(x, y, it.x, it.y);
|
|
if (isConstraint) {
|
|
const runtime = global.TarinaiLinkRuntime;
|
|
const pb = global.TarinaiPhysicsBodySystem;
|
|
const a = runtime?.endpointWorld?.(pb?.endpoint?.(it, 0), this);
|
|
const b = runtime?.endpointWorld?.(pb?.endpoint?.(it, 1), this);
|
|
if (!a || !b) continue;
|
|
d = global.TarinaiGeometry.pointSegmentDistance(x, y, a.x, a.y, b.x, b.y);
|
|
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;
|
|
}
|
|
if ((this.tool === "rope" || this.tool === "rod" || this.tool === "spring") && bestD > 14) {
|
|
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
|
|
const it = this.items[i];
|
|
if (!it || it.dead || !global.TarinaiMechanicalSystem.isMechanicalType(it.type)) continue;
|
|
const reach = global.TarinaiMechanicalSystem.reach?.(it) || this.rotatorExtent?.(it) || Math.max(64, it.r || 64);
|
|
if (distXY(x, y, it.x, it.y) > reach + 32) continue;
|
|
const result = global.TarinaiCollisionFootprints.hitTestItem(this, it, x, y, { padding: 26, radiusMultiplier: 1.45 }) || { hit: false, distance: Infinity };
|
|
if (!result.hit || result.distance >= bestD) continue;
|
|
const local = this.linkPointLocalForItem?.(it, x, y, true) || { localX: 0, localY: 0 };
|
|
best = { kind: "item", id: it.id, type: it.type, label: toolLabel(it.type), x, y, localX: local.localX, localY: local.localY, center: false };
|
|
global.TarinaiLinkRuntime.snapEndpointToTarget(best, this);
|
|
bestD = result.distance;
|
|
}
|
|
}
|
|
return best;
|
|
},
|
|
|
|
useLinkToolAt(x, y) {
|
|
global.TarinaiAchievements?.recordIntervention?.({ world: this, tool: this.tool, x, y });
|
|
const endpoint = this.findLinkEndpointAt?.(x, y);
|
|
const linkNames = { rope: "\u30d2\u30e2", rod: "\u68d2", spring: "\u30d0\u30cd", wire: "\u96fb\u7dda", insulated_wire: "\u7d76\u7e01\u96fb\u7dda" };
|
|
const toolName = linkNames[this.tool] || "\u63a5\u7d9a";
|
|
if (!endpoint) { showToast(`${toolName}\u3092\u3064\u306a\u3050\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002`); return true; }
|
|
if (!this.pendingLinkEndpoint) {
|
|
this.pendingLinkEndpoint = endpoint;
|
|
showToast(`${endpoint.label || "\u5bfe\u8c61"}\u306b${toolName}\u306e\u7247\u7aef\u3092\u3064\u306a\u304e\u307e\u3057\u305f\u3002\u3082\u3046\u7247\u7aef\u3092\u9078\u3093\u3067\u304f\u3060\u3055\u3044\u3002`);
|
|
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("\u5225\u306e\u4f4d\u7f6e\u307e\u305f\u306f\u5bfe\u8c61\u3092\u9078\u3093\u3067\u304f\u3060\u3055\u3044\u3002");
|
|
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 = ["rod", "spring", "wire", "insulated_wire"].includes(this.tool) ? this.tool : "rope";
|
|
const item = new Item(constraintType, (p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);
|
|
item.world = this;
|
|
const pb = global.TarinaiPhysicsBodySystem;
|
|
pb?.ensureConstraint?.(item, this, { syncFromLegacy: false });
|
|
pb?.setEndpoint?.(item, 0, { ...first, x: p1.x, y: p1.y });
|
|
pb?.setEndpoint?.(item, 1, { ...endpoint, x: p2.x, y: p2.y });
|
|
const linkLen = Math.max(24, distXY(p1.x, p1.y, p2.x, p2.y));
|
|
pb?.setLinkScalar?.(item, "len", linkLen);
|
|
pb?.setLinkScalar?.(item, "midX", (p1.x + p2.x) * 0.5);
|
|
pb?.setLinkScalar?.(item, "midY", (p1.y + p2.y) * 0.5);
|
|
pb?.setLinkScalar?.(item, "midVx", 0);
|
|
pb?.setLinkScalar?.(item, "midVy", 0);
|
|
item.r = Math.max(18, linkLen * 0.5);
|
|
item.amount = 999;
|
|
const placed = this.addItem?.(item, constraintType);
|
|
this.pendingLinkEndpoint = null;
|
|
if (!placed) { showToast(`${toolName}\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002`); return true; }
|
|
global.TarinaiAchievements?.recordLinkPlaced?.(constraintType, { world: this, item, type: constraintType });
|
|
global.TarinaiPhysicsSoftClear.clearForItem(this, item, `${constraintType}-soft-overlap-clear`);
|
|
this._forceImmediateToolVisualRefresh?.("tool-action");
|
|
this.log?.(`${toolName}\u3092\u3064\u306a\u3044\u3060\u3002`, "observe");
|
|
showToast(`${toolName}\u3092\u3064\u306a\u304e\u307e\u3057\u305f\u3002`);
|
|
return true;
|
|
},
|
|
|
|
shootAimPoint() {
|
|
const p = this.pointer || {};
|
|
const pad = Math.max(0, CONFIG.worldPadding || 0);
|
|
const x = clamp((Number(p.x) || 0) + (Number(this.shootCursorOffsetX) || 0), pad, Math.max(pad, (this.w || 0) - pad));
|
|
const y = clamp((Number(p.y) || 0) + (Number(this.shootCursorOffsetY) || 0), pad, Math.max(pad, (this.h || 0) - pad));
|
|
return { x, y, inside: Boolean(p.inside) };
|
|
},
|
|
|
|
setShootCursorFromPointer(p = this.pointer || {}) {
|
|
this.shootPointerRawX = Number(p.x) || 0;
|
|
this.shootPointerRawY = Number(p.y) || 0;
|
|
this.shootCursorActive = true;
|
|
return this.shootAimPoint?.() || { x: this.shootPointerRawX, y: this.shootPointerRawY };
|
|
},
|
|
|
|
moveShootCursorBy() {
|
|
// Mouse/touch movement should move the base pointer, not accumulate extra
|
|
// logical drift. Only recoil changes shootCursorOffsetX/Y.
|
|
this.shootCursorActive = true;
|
|
this.drawListDirty = true;
|
|
return this.shootAimPoint?.() || { x: this.pointer?.x || 0, y: this.pointer?.y || 0 };
|
|
},
|
|
|
|
applyShootToolRecoil(power = 1) {
|
|
const unit = this.screenSizeToWorld ? this.screenSizeToWorld(rand(18, 34)) : rand(18, 34);
|
|
const mag = Math.max(8, unit) * Math.max(0.65, Number(power) || 1);
|
|
const angle = -Math.PI / 2 + rand(-0.52, 0.52);
|
|
const dx = Math.cos(angle) * mag;
|
|
const dy = Math.sin(angle) * mag;
|
|
this.shootCursorOffsetX = clamp((Number(this.shootCursorOffsetX) || 0) + dx, -130, 130);
|
|
this.shootCursorOffsetY = clamp((Number(this.shootCursorOffsetY) || 0) + dy, -150, 70);
|
|
this.shootCursorActive = true;
|
|
this.drawListDirty = true;
|
|
return this.shootAimPoint?.() || { x: this.pointer?.x || 0, y: this.pointer?.y || 0 };
|
|
},
|
|
|
|
findShootToolTargetAt(x, y) {
|
|
let best = null;
|
|
let bestD = Infinity;
|
|
const tarinaiCandidates = this.nearbyTarinai?.(x, y, 82, true) || this.tarinai || [];
|
|
for (let i = tarinaiCandidates.length - 1; i >= 0; i -= 1) {
|
|
const t = tarinaiCandidates[i];
|
|
if (!t || t.dead || this.isTarinaiHiddenInNestBox?.(t)) continue;
|
|
const hit = t.contains ? t.contains(x, y) : distXY(x, y, t.x, t.y) <= (t.radius || 22) * 1.28;
|
|
const d = distXY(x, y, t.x, t.y);
|
|
if (hit && d < bestD) { best = { kind: "tarinai", target: t, distance: d }; bestD = d; }
|
|
}
|
|
const antCandidates = this.nearbyAnts?.(x, y, 32, true) || this.ants || [];
|
|
for (let i = antCandidates.length - 1; i >= 0; i -= 1) {
|
|
const ant = antCandidates[i];
|
|
if (!ant || ant.dead) continue;
|
|
const hitR = Math.max(6, (ant.r || 6) * 1.35);
|
|
const d = distXY(x, y, ant.x, ant.y);
|
|
if (d <= hitR && d < bestD) { best = { kind: "ant", target: ant, distance: d }; bestD = d; }
|
|
}
|
|
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
|
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
|
const it = itemCandidates[i];
|
|
if (!it || it.dead || it.type === "trace" || it.type === "splat") continue;
|
|
let d = distXY(x, y, it.x, it.y);
|
|
let hit = false;
|
|
if (["rope", "rod", "spring", "wire", "insulated_wire"].includes(it.type)) {
|
|
const runtime = global.TarinaiLinkRuntime;
|
|
const pb = global.TarinaiPhysicsBodySystem;
|
|
const a = runtime?.endpointWorld?.(pb?.endpoint?.(it, 0), this);
|
|
const b = runtime?.endpointWorld?.(pb?.endpoint?.(it, 1), this);
|
|
d = global.TarinaiGeometry.pointSegmentDistance(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y);
|
|
hit = d < 18;
|
|
} else if (global.TarinaiMechanicalSystem.isMechanicalType(it.type)) {
|
|
const result = global.TarinaiMechanicalSystem.hitTest(this, it, x, y, { padding: 14 });
|
|
hit = Boolean(result.hit);
|
|
d = Math.min(d, result.distance ?? d);
|
|
} else {
|
|
const result = global.TarinaiCollisionFootprints.hitTestItem(this, it, x, y, { radiusMultiplier: (it.type === "ball" || it.type === "balloon") ? 3.2 : 1.65, padding: 10 }) || { hit: false, distance: d };
|
|
hit = Boolean(result.hit);
|
|
d = Math.min(d, result.distance ?? d);
|
|
}
|
|
if (hit && d < bestD) { best = { kind: "item", target: it, distance: d }; bestD = d; }
|
|
}
|
|
return best;
|
|
},
|
|
|
|
shootFleeTargetFor(t, x, y, minDist = 150, maxDist = 250) {
|
|
if (!t) return null;
|
|
const dx = (Number(t.x) || 0) - (Number(x) || 0);
|
|
const dy = (Number(t.y) || 0) - (Number(y) || 0);
|
|
const d = Math.hypot(dx, dy);
|
|
const a = d > 3 ? Math.atan2(dy, dx) : rand(-Math.PI, Math.PI);
|
|
const fleeDist = rand(minDist, maxDist);
|
|
const pad = Math.max(CONFIG.worldPadding || 0, (t.radius || 20) + 8);
|
|
return {
|
|
x: clamp((Number(t.x) || 0) + Math.cos(a) * fleeDist, pad, Math.max(pad, (this.w || 0) - pad)),
|
|
y: clamp((Number(t.y) || 0) + Math.sin(a) * fleeDist, pad, Math.max(pad, (this.h || 0) - pad)),
|
|
dead: false,
|
|
detour: true,
|
|
panic: true,
|
|
};
|
|
},
|
|
|
|
panicTarinaiFromShootImpact(t, x, y, options = {}) {
|
|
if (!t || t.dead || this.isTarinaiHiddenInNestBox?.(t)) return false;
|
|
const direct = Boolean(options.direct);
|
|
const strength = Math.max(0.25, Number(options.strength) || 1);
|
|
const target = this.shootFleeTargetFor?.(t, x, y, direct ? 170 : 115, direct ? 260 : 205) || null;
|
|
const reason = direct ? "\u6483\u305f\u308c\u3066\u30d1\u30cb\u30c3\u30af\u3067\u9003\u3052\u3066\u3044\u308b" : "\u8fd1\u304f\u306b\u5f3e\u304c\u5f53\u305f\u3063\u3066\u30d1\u30cb\u30c3\u30af\u3067\u9003\u3052\u3066\u3044\u308b";
|
|
const threat = { x: Number(x) || t.x, y: Number(y) || t.y, type: "shoot", dead: false };
|
|
t.fearTimer = Math.max(t.fearTimer || 0, (direct ? 2.8 : 1.25) * strength);
|
|
t.surpriseTimer = Math.max(t.surpriseTimer || 0, (direct ? 0.34 : 0.28) * strength);
|
|
if (direct) t.hurtTimer = Math.max(t.hurtTimer || 0, 1.35);
|
|
if (t.enterPanic) t.enterPanic({ threat, destination: target, target, forceDestination: true, reason, fear: direct ? 1.25 : 0.85 * strength, wake: true, cause: direct ? "shoot_tool" : "shoot_impact_nearby", hurtTimer: direct ? 1.35 : undefined, surpriseTimer: direct ? 0.34 : 0.25, awakeLockTimer: direct ? 1.0 : 0.45 });
|
|
t.setActionState?.("panic", { target, reason, wake: true, sleeping: false });
|
|
t.target = target;
|
|
t.panicTarget = target;
|
|
t.panicTargetUntil = (this.time || 0) + (direct ? 2.4 : 1.55 * strength);
|
|
const dx = (Number(t.x) || 0) - (Number(x) || 0);
|
|
const dy = (Number(t.y) || 0) - (Number(y) || 0);
|
|
const d = Math.hypot(dx, dy);
|
|
const a = d > 3 ? Math.atan2(dy, dx) : rand(-Math.PI, Math.PI);
|
|
t.vx = clamp((t.vx || 0) + Math.cos(a) * rand(direct ? 28 : 12, direct ? 56 : 30) * strength, -220, 220);
|
|
t.vy = clamp((t.vy || 0) + Math.sin(a) * rand(direct ? 24 : 10, direct ? 48 : 26) * strength, -220, 220);
|
|
return true;
|
|
},
|
|
|
|
panicTarinaiAroundShootImpact(x, y, directTarget = null) {
|
|
const radius = 138;
|
|
let count = 0;
|
|
const candidates = this.nearbyTarinai?.(x, y, radius + 32, true) || this.tarinai || [];
|
|
for (const t of candidates) {
|
|
if (!t || t.dead || t === directTarget || this.isTarinaiHiddenInNestBox?.(t)) continue;
|
|
const d = distXY(x, y, t.x, t.y);
|
|
if (d > radius) continue;
|
|
const strength = clamp(1 - d / radius, 0.22, 1.0);
|
|
if (this.panicTarinaiFromShootImpact?.(t, x, y, { direct: false, strength })) count += 1;
|
|
}
|
|
return count;
|
|
},
|
|
|
|
fireShootTool(x = null, y = null) {
|
|
const aim = (x == null || y == null) ? this.shootAimPoint?.() : { x: Number(x) || 0, y: Number(y) || 0, inside: true };
|
|
if (!aim?.inside) return false;
|
|
global.TarinaiAchievements?.recordShotFired?.({ world: this, x: aim.x, y: aim.y });
|
|
global.TarinaiAchievements?.recordIntervention?.({ world: this, tool: "shoot", x: aim.x, y: aim.y });
|
|
const hit = this.findShootToolTargetAt?.(aim.x, aim.y);
|
|
audio.shoot?.();
|
|
this.effects?.push(new Effect("shoot_impact", aim.x, aim.y, { size: hit ? 22 : 16, life: hit ? 0.28 : 0.22, color: hit ? "rgba(255,208,116,0.96)" : "rgba(190,212,240,0.64)" }));
|
|
this.effects?.push(new Effect("ring", aim.x, aim.y, { size: hit ? 20 : 14, life: hit ? 0.18 : 0.14, color: hit ? "rgba(255,246,206,0.74)" : "rgba(196,220,255,0.42)" }));
|
|
let handled = false;
|
|
if (hit?.kind === "tarinai") {
|
|
const t = hit.target;
|
|
const damage = 16;
|
|
t.damage?.(damage, "\u5c04\u6483", { source: { id: "shoot", type: "shoot" } });
|
|
t.hurtTimer = Math.max(t.hurtTimer || 0, 1.35);
|
|
t.pokeFlashTimer = Math.max(t.pokeFlashTimer || 0, 0.22);
|
|
t.fearTimer = Math.max(t.fearTimer || 0, 2.8);
|
|
this.panicTarinaiFromShootImpact?.(t, aim.x, aim.y, { direct: true, strength: 1 });
|
|
handled = true;
|
|
} else if (hit?.kind === "ant") {
|
|
const ant = hit.target;
|
|
const damage = ant.kind === "queen" ? 24 : 34;
|
|
global.TarinaiAchievements?.recordAntDamageSource?.(ant, { id: "shoot", type: "shoot" }, { world: this });
|
|
ant.hp = Math.max(0, (Number(ant.hp) || Number(ant.maxHp) || 0) - damage);
|
|
ant.hpBarTimer = Math.max(ant.hpBarTimer || 0, 1.3);
|
|
if (ant.hp <= 0) ant.dieAsCorpse?.(ant.homeNest?.() || null);
|
|
handled = true;
|
|
} else if (hit?.kind === "item") {
|
|
handled = true;
|
|
const target = hit.target;
|
|
if (target?.type === "balloon") {
|
|
globalThis.TarinaiItemDynamicBallSystem?.popBalloon?.(target, this, { type: "shoot", x: aim.x, y: aim.y, id: "shoot" });
|
|
this.markItemBucketsDirty?.("shoot-water-balloon-pop");
|
|
this.markSpatialDirty?.("shoot-water-balloon-pop");
|
|
this.drawListDirty = true;
|
|
} else if (target && isPinType(target.type)) {
|
|
const blasted = this.blastLoosePinsFrom?.(aim.x, aim.y, 92, target, 1.25, "shoot-tool") || 0;
|
|
if (!blasted && target.pinState !== "lodged") {
|
|
let dx = (target.x || 0) - aim.x;
|
|
let dy = (target.y || 0) - aim.y;
|
|
let d = Math.hypot(dx, dy) || 1;
|
|
if (d < 0.001) {
|
|
const a = deterministicAngle(this, "shoot-pin-overlap", target, aim.x, aim.y);
|
|
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
|
}
|
|
const pinSystem = globalThis.TarinaiItemDynamicPinSystem;
|
|
if (target.pinState === "ball_lodged") pinSystem?.detach?.(target, this, "shoot");
|
|
this.applyImpulse?.(target,
|
|
dx / d * 610 + deterministicRange(this, "shoot-pin-jitter-x", -70, 70, target, aim.x, aim.y),
|
|
dy / d * 610 + deterministicRange(this, "shoot-pin-jitter-y", -70, 70, target, aim.x, aim.y)
|
|
);
|
|
target.spinVelocity = clamp((target.spinVelocity || 0) + deterministicRange(this, "shoot-pin-spin", -20, 20, target, aim.x, aim.y), -40, 40);
|
|
target.noStickUntil = (this.time || 0) + 0.22;
|
|
target.lastPokedAt = this.time || 0;
|
|
}
|
|
this.effects?.push(new Effect("ring", target.x || aim.x, target.y || aim.y, { size: Math.max(12, (target.r || 8) * 1.8), life: 0.18, color: "rgba(255,230,116,0.48)" }));
|
|
this.markSpatialDirty?.("shoot-pin-blast");
|
|
this.drawListDirty = true;
|
|
} else {
|
|
const shouldRemove = Math.random() < 0.30;
|
|
if (shouldRemove) {
|
|
const removed = global.TarinaiStructureLifecycle.deleteItem(this, target, {
|
|
reason: "shoot-tool",
|
|
userReason: "\u5c04\u6483\u3067\u6d88\u3048\u305f",
|
|
wake: true,
|
|
});
|
|
if (removed) {
|
|
this.markItemBucketsDirty?.("shoot-delete");
|
|
this.markSpatialDirty?.("shoot-delete");
|
|
this.drawListDirty = true;
|
|
} else {
|
|
}
|
|
} else {
|
|
}
|
|
}
|
|
} else {
|
|
}
|
|
this.panicTarinaiAroundShootImpact?.(aim.x, aim.y, hit?.kind === "tarinai" ? hit.target : null);
|
|
this.lastShootAt = this.time || 0;
|
|
this.applyShootToolRecoil?.(handled ? 1 : 0.88);
|
|
this.drawListDirty = true;
|
|
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 (["rope", "rod", "spring", "wire", "insulated_wire"].includes(this.tool)) return this.useLinkToolAt(x, y);
|
|
return false;
|
|
},
|
|
}));
|
|
})(window);
|