tarinai/js/world_tool_actions.js
2026-07-16 22:12:03 +09:00

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);