"use strict"; // Signal network, rectangular detectors, and conductive wires. (function (global) { const WIRE_TYPES = new Set(["wire", "insulated_wire"]); const LINK_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]); const SIGNAL_SOURCE_TYPES = new Set(["pressure_switch"]); const DIRECT_SIGNAL_SINK_TYPES = new Set(["rotator", "reciprocator", "robot_cleaner", "gate_fence", "fan", "stove", "dry_ice", "duplicator"]); const DETECTOR_METRICS = Object.freeze({ tarinai_count: { label: "\u305f\u308a\u306a\u3044\u6570", defaultThreshold: 1 }, hunger_avg: { label: "\u5e73\u5747\u7a7a\u8179", defaultThreshold: 70 }, stress_avg: { label: "\u5e73\u5747\u30b9\u30c8\u30ec\u30b9", defaultThreshold: 70 }, sleep_avg: { label: "\u5e73\u5747\u7761\u7720\u6b32", defaultThreshold: 70 }, low_hp_count: { label: "\u4f4eHP\u500b\u4f53\u6570", defaultThreshold: 1 }, sick_count: { label: "\u75c5\u6c17\u500b\u4f53\u6570", defaultThreshold: 1 }, temperature: { label: "\u6c17\u6e29", defaultThreshold: 15 }, zunchi_count: { label: "\u305a\u3093\u3061\u6570", defaultThreshold: 1 }, ant_count: { label: "\u30a2\u30ea\u6570", defaultThreshold: 1 }, water_count: { label: "\u6c34\u6ef4\u6570", defaultThreshold: 1 }, }); const PRESSURE_TARGETS = Object.freeze({ tarinai: { label: "\u305f\u308a\u306a\u3044\u6570", defaultThreshold: 1, spatial: true }, item: { label: "\u30a2\u30a4\u30c6\u30e0\u6570", defaultThreshold: 1, spatial: true }, time: { label: "\u6642\u523b", defaultThreshold: 0, spatial: false }, hunger_avg: DETECTOR_METRICS.hunger_avg, stress_avg: DETECTOR_METRICS.stress_avg, sleep_avg: DETECTOR_METRICS.sleep_avg, low_hp_count: DETECTOR_METRICS.low_hp_count, sick_count: DETECTOR_METRICS.sick_count, temperature: { ...DETECTOR_METRICS.temperature, spatial: false }, zunchi_count: DETECTOR_METRICS.zunchi_count, ant_count: DETECTOR_METRICS.ant_count, water_count: DETECTOR_METRICS.water_count, }); const num = global.TarinaiCoreHelpers?.finiteOr || ((value, fallback = 0) => { const n = Number(value); return Number.isFinite(n) ? n : fallback; }); const clamp = global.TarinaiCoreHelpers?.clampNumber || ((value, min, max) => Math.max(min, Math.min(max, num(value, min)))); function normalizeMinute(value, fallback = 0) { return Math.max(0, Math.min(1435, Math.round(num(value, fallback) / 5) * 5)); } function currentMinuteOfDay(worldRef) { const dayLength = Math.max(1, num(worldRef?.config?.dayLength ?? global.CONFIG?.dayLength, 120)); const progress = typeof worldRef?.dayProgress === "function" ? num(worldRef.dayProgress()) : ((num(worldRef?.time) % dayLength) + dayLength) % dayLength / dayLength; return Math.max(0, Math.min(1439, Math.floor((((progress % 1) + 1) % 1) * 1440))); } function isMinuteInRange(minute, start, end) { const now = Math.max(0, Math.min(1439, Number(minute) | 0)); const from = normalizeMinute(start, 360); const to = normalizeMinute(end, 1080); if (from === to) return true; return from < to ? now >= from && now < to : now >= from || now < to; } function normalizePressureTarget(value) { return PRESSURE_TARGETS[value] ? value : "tarinai"; } function normalizeComparator(value) { return value === "lte" ? "lte" : "gte"; } function isWire(item) { return Boolean(item && !item.dead && WIRE_TYPES.has(item.type)); } function emitEffect(worldRef, type, x, y, options = {}) { if (!worldRef?.effects) return false; const EffectCtor = global.TarinaiEffect || (typeof Effect === "function" ? Effect : null); if (typeof EffectCtor !== "function") return false; worldRef.effects.push(new EffectCtor(type, x, y, options)); return true; } function endpointObject(endpoint, worldRef) { if (!endpoint || !worldRef) return null; const cached = endpoint._ref; if (cached && !cached.dead && (!endpoint.id || cached.id === endpoint.id)) return cached; let found = null; if (endpoint.kind === "tarinai") { found = worldRef.liveTarinaiById?.(endpoint.id, endpoint.liveToken ?? null) || worldRef.tarinaiById?.get?.(endpoint.id) || (worldRef.tarinai || []).find(t => t && !t.dead && t.id === endpoint.id) || null; } else if (endpoint.kind === "ant") { found = (worldRef.ants || []).find(ant => ant && !ant.dead && ant.id === endpoint.id) || null; } else { found = worldRef.itemById?.(endpoint.id) || (worldRef.items || []).find(item => item && !item.dead && item.id === endpoint.id) || null; } endpoint._ref = found; return found; } function endpointSignalNode(endpoint, object, worldRef) { if (!endpoint || !object) return object || null; if (object.type !== "circuit_board") return object; const circuit = global.TarinaiCircuitBoardSystem; const portId = endpoint.portId || circuit?.nearestPort?.(object, endpoint.x, endpoint.y, 80)?.port?.portId || ""; return circuit?.terminal?.(object, portId) || null; } function isSignalConnectable(item) { return Boolean(item && !item.dead && (SIGNAL_SOURCE_TYPES.has(item.type) || DIRECT_SIGNAL_SINK_TYPES.has(item.type) || item.type === "circuit_board")); } function endpointPoint(endpoint, worldRef) { return global.TarinaiLinkRuntime?.endpointWorld?.(endpoint, worldRef) || (endpoint ? { x: num(endpoint.x), y: num(endpoint.y) } : null); } function insideRect(item, target, width, height) { const tr = Math.max(0, num(target?.r ?? target?.radius)); return Math.abs(num(target?.x) - num(item.x)) <= width * 0.5 + tr && Math.abs(num(target?.y) - num(item.y)) <= height * 0.5 + tr; } function detectorTarinai(item, worldRef, width, height) { const radius = Math.hypot(width * 0.5, height * 0.5) + 48; return (worldRef.nearbyTarinai?.(item.x, item.y, radius, true) || worldRef.tarinai || []) .filter(t => t && !t.dead && !worldRef.isTarinaiHiddenInNestBox?.(t) && insideRect(item, t, width, height)); } function detectorItems(item, worldRef, width, height, type = "") { const radius = Math.hypot(width * 0.5, height * 0.5) + 48; return (worldRef.nearbyItems?.(item.x, item.y, radius, true) || worldRef.items || []) .filter(it => it && !it.dead && it !== item && !LINK_TYPES.has(it.type) && (!type || it.type === type) && insideRect(item, it, width, height)); } function average(list, valueFor) { if (!list.length) return 0; return list.reduce((sum, value) => sum + num(valueFor(value)), 0) / list.length; } function detectorMetricValue(item, worldRef) { const width = clamp(item.pressureWidth || 220, 60, 840); const height = clamp(item.pressureHeight || 160, 60, 840); const target = normalizePressureTarget(item.pressureTarget); if (target === "temperature") return num(worldRef.temperatureAt?.(item.x, item.y, null), global.CONFIG?.standardTemperature ?? 15); if (target === "ant_count") { const radius = Math.hypot(width * 0.5, height * 0.5) + 32; return (worldRef.nearbyAnts?.(item.x, item.y, radius, true) || worldRef.ants || []) .filter(a => a && !a.dead && insideRect(item, a, width, height)).length; } if (target === "item") return detectorItems(item, worldRef, width, height).length; if (target === "zunchi_count") return detectorItems(item, worldRef, width, height, "zunchi").length; if (target === "water_count") return detectorItems(item, worldRef, width, height, "water").length; const actors = detectorTarinai(item, worldRef, width, height); if (target === "tarinai") return actors.length; if (target === "hunger_avg") return average(actors, t => t.hunger); if (target === "stress_avg") return average(actors, t => t.stress); if (target === "sleep_avg") return average(actors, t => t.needs?.sleep ?? t.needRaw?.sleep ?? t.sleepPressure); if (target === "low_hp_count") return actors.filter(t => num(t.hp, num(t.maxHp, 100)) / Math.max(1, num(t.maxHp, 100)) <= 0.35).length; if (target === "sick_count") return actors.filter(t => t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease).length; return actors.length; } function updatePressureSwitches(worldRef) { const minute = currentMinuteOfDay(worldRef); for (const item of worldRef.itemsOfType?.("pressure_switch") || []) { if (!item || item.dead) continue; item.pressureTarget = normalizePressureTarget(item.pressureTarget); const defaultThreshold = PRESSURE_TARGETS[item.pressureTarget]?.defaultThreshold ?? 1; item.pressureMin = clamp(num(item.pressureMin, defaultThreshold), -999, 999); item.pressureMax = clamp(num(item.pressureMax, 999), -999, 999); if (item.pressureMin > item.pressureMax) { const swap = item.pressureMin; item.pressureMin = item.pressureMax; item.pressureMax = swap; } item.pressureWidth = clamp(item.pressureWidth || 220, 60, 840); item.pressureHeight = clamp(item.pressureHeight || 160, 60, 840); item.pressureTimeStart = normalizeMinute(item.pressureTimeStart, 360); item.pressureTimeEnd = normalizeMinute(item.pressureTimeEnd, 1080); let value = 0; let active = false; if (item.pressureTarget === "time") { active = isMinuteInRange(minute, item.pressureTimeStart, item.pressureTimeEnd); value = active ? 1 : 0; } else { value = detectorMetricValue(item, worldRef); active = value >= item.pressureMin && value <= item.pressureMax; } if (active !== Boolean(item.signalActive)) worldRef.drawListDirty = true; item.pressureCount = value; item.pressureValue = value; item.pressureCurrentMinute = minute; item.signalActive = active; } } function setEffectivePower(node, directDriven, directOn, worldRef) { const wasOn = Boolean(node?._achievementSignalWasOn); node._directSignalDriven = Boolean(directDriven); node._directSignalOn = Boolean(directDriven && directOn); node._achievementSignalWasOn = node._directSignalOn; if (!wasOn && node._directSignalOn) { global.TarinaiAchievements?.recordSignalActivation?.({ world: worldRef, target: node, type: node.type }); if (node.type === "robot_cleaner") global.TarinaiAchievements?.evaluateCleanFreak?.(worldRef, { robot: node, source: "signal-power" }); } node._signalDriven = node._directSignalDriven; node._signalOn = node._directSignalOn; if (node.type === "gate_fence" && node._signalDriven) { const nextOpen = Boolean(node._signalOn); if (Boolean(node.gateOpen) !== nextOpen) { node.gateOpen = nextOpen; worldRef.drawListDirty = true; worldRef.markSpatialDirty?.("signal-gate-toggle"); } } } function buildSignalNetwork(worldRef) { const adjacency = new Map(); const connectedNodes = new Set(); const wires = []; const circuit = global.TarinaiCircuitBoardSystem; let wireGeometryMoved = false; const addEdge = (a, b) => { if (!a || !b || a === b) return; if (!adjacency.has(a)) adjacency.set(a, new Set()); if (!adjacency.has(b)) adjacency.set(b, new Set()); adjacency.get(a).add(b); adjacency.get(b).add(a); connectedNodes.add(a); connectedNodes.add(b); }; const boards = []; for (const item of worldRef.items || []) { if (!item || item.dead) continue; if (DIRECT_SIGNAL_SINK_TYPES.has(item.type)) { item._wireSignalDriven = false; item._wireSignalOn = false; item._directSignalDriven = false; item._directSignalOn = false; } if (item.type === "circuit_board") { circuit?.ensureConfig?.(item); item._circuitInputState = Object.create(null); if (!item._circuitOutputState || typeof item._circuitOutputState !== "object") circuit?.evaluate?.(item, item._circuitInputState); boards.push(item); } } for (const wire of worldRef.items || []) { if (!isWire(wire)) continue; const pb = global.TarinaiPhysicsBodySystem; const endpointA = pb?.endpoint?.(wire, 0); const endpointB = pb?.endpoint?.(wire, 1); const objectA = endpointObject(endpointA, worldRef); const objectB = endpointObject(endpointB, worldRef); const nodeA = endpointSignalNode(endpointA, objectA, worldRef); const nodeB = endpointSignalNode(endpointB, objectB, worldRef); const pointA = endpointPoint(endpointA, worldRef); const pointB = endpointPoint(endpointB, worldRef); if (pointA && pointB) { const nextX = (pointA.x + pointB.x) * 0.5; const nextY = (pointA.y + pointB.y) * 0.5; const nextR = Math.max(18, Math.hypot(pointB.x - pointA.x, pointB.y - pointA.y) * 0.5); if (Math.hypot(num(wire.x) - nextX, num(wire.y) - nextY) > 0.25 || Math.abs(num(wire.r) - nextR) > 0.25) wireGeometryMoved = true; wire.x = nextX; wire.y = nextY; wire.r = nextR; if (endpointA) { endpointA.x = pointA.x; endpointA.y = pointA.y; } if (endpointB) { endpointB.x = pointB.x; endpointB.y = pointB.y; } } wires.push({ wire, a: objectA, b: objectB, nodeA, nodeB }); if (nodeA && nodeB) addEdge(nodeA, nodeB); } const components = []; const componentOf = new Map(); const visited = new Set(); for (const startNode of connectedNodes) { if (visited.has(startNode)) continue; const stack = [startNode], nodes = []; while (stack.length) { const node = stack.pop(); if (!node || visited.has(node)) continue; visited.add(node); nodes.push(node); for (const next of adjacency.get(node) || []) if (!visited.has(next)) stack.push(next); } const component = { nodes, active: false, hasSource: nodes.some(node => SIGNAL_SOURCE_TYPES.has(node.type) || (node.type === "circuit_terminal" && node.direction === "output")) }; components.push(component); nodes.forEach(node => componentOf.set(node, component)); } // Board outputs can depend on other boards, so resolve the graph to a // fixed point. Unstable internal combinational cycles are forced OFF by the // board evaluator; external feedback is still bounded to avoid hanging the simulation. const maxCircuitPasses = Math.min(256, Math.max(16, boards.length * 2 + 4)); for (let pass = 0; pass < maxCircuitPasses; pass += 1) { let changed = false; for (const component of components) { const active = component.hasSource && component.nodes.some(node => (SIGNAL_SOURCE_TYPES.has(node.type) && node.signalActive) || (node.type === "circuit_terminal" && node.direction === "output" && node.signalActive)); if (active !== component.active) { component.active = active; changed = true; } } for (const board of boards) { const inputs = Object.create(null); for (const port of circuit?.ports?.(board) || []) { if (port.direction !== "input") continue; const terminal = circuit.terminal(board, port.portId); inputs[port.id] = Boolean(componentOf.get(terminal)?.active); } const before = JSON.stringify(board._circuitOutputState || {}); circuit?.evaluate?.(board, inputs); if (before !== JSON.stringify(board._circuitOutputState || {})) changed = true; } if (!changed) break; } for (const component of components) { component.active = component.hasSource && component.nodes.some(node => (SIGNAL_SOURCE_TYPES.has(node.type) && node.signalActive) || (node.type === "circuit_terminal" && node.direction === "output" && node.signalActive)); for (const node of component.nodes) { if (!DIRECT_SIGNAL_SINK_TYPES.has(node.type)) continue; node._wireSignalDriven = component.hasSource; node._wireSignalOn = component.hasSource && component.active; setEffectivePower(node, component.hasSource, component.active, worldRef); } } for (const entry of wires) { const component = componentOf.get(entry.nodeA) || componentOf.get(entry.nodeB); entry.wire.signalActive = Boolean(component?.hasSource && component?.active); if (!entry.wire.signalActive || entry.a === entry.b) continue; let sourceNode = null, targetNode = null, sourceBoard = null, targetBoard = null; if (entry.nodeA?.type === "circuit_terminal" && entry.nodeA.direction === "output" && entry.nodeB?.type === "circuit_terminal" && entry.nodeB.direction === "input") { sourceNode = entry.nodeA; targetNode = entry.nodeB; sourceBoard = entry.a; targetBoard = entry.b; } else if (entry.nodeB?.type === "circuit_terminal" && entry.nodeB.direction === "output" && entry.nodeA?.type === "circuit_terminal" && entry.nodeA.direction === "input") { sourceNode = entry.nodeB; targetNode = entry.nodeA; sourceBoard = entry.b; targetBoard = entry.a; } if (!sourceNode || !targetNode || sourceBoard === targetBoard) continue; const sourcePortId = String(sourceNode.portId || "").replace(/^o:/, ""); const targetPortId = String(targetNode.portId || "").replace(/^i:/, ""); if (!sourceBoard?._circuitOutputState?.[sourcePortId] || !targetBoard?._circuitInputState?.[targetPortId]) continue; global.TarinaiAchievements?.recordCircuitBoardSignalTransfer?.(sourceBoard, targetBoard, entry.wire, { world: worldRef }); } for (const item of worldRef.items || []) { if (!item || item.dead) continue; if (DIRECT_SIGNAL_SINK_TYPES.has(item.type) && !componentOf.has(item)) { item._wireSignalDriven = false; item._wireSignalOn = false; setEffectivePower(item, false, false, worldRef); } if (isWire(item) && !wires.some(entry => entry.wire === item)) item.signalActive = false; } if (wireGeometryMoved) { worldRef.drawListDirty = true; worldRef.markSpatialDirty?.("signal-wire-motion"); } return wires; } const distanceToSegment = (px, py, ax, ay, bx, by) => global.TarinaiGeometry.pointSegmentDistance(px, py, ax, ay, bx, by); function shockTarget(worldRef, wire, target, now, { connected = false } = {}) { if (!target || target.dead) return false; if (!wire._shockCooldowns || typeof wire._shockCooldowns.get !== "function") wire._shockCooldowns = new WeakMap(); if (now < num(wire._shockCooldowns.get(target), -Infinity)) return false; wire._shockCooldowns.set(target, now + 0.38); const isAnt = target.kind === "worker" || target.kind === "queen" || (target.r && !target.radius && Number.isFinite(Number(target.hp))); if (isAnt) { globalThis.TarinaiAchievements?.recordAntDamageSource?.(target, wire, { world: worldRef, reason: "electric_wire" }); target.hp = Math.max(0, num(target.hp, target.maxHp || 32) - 2.4); target.hpBarTimer = Math.max(num(target.hpBarTimer), 0.85); target.state = "panic"; target.targetId = ""; target.targetToken = 0; target.targetRef = null; target.electricShockUntil = Math.max(num(target.electricShockUntil), now + 0.55); } else { target.damage?.(2.4, connected ? "\u901a\u96fb\u4e2d\u306e\u96fb\u7dda\u306b\u63a5\u7d9a\u3055\u308c\u305f" : "\u901a\u96fb\u4e2d\u306e\u96fb\u7dda\u306b\u89e6\u308c\u305f"); target.panicTimer = Math.max(num(target.panicTimer), 0.8); target.fearTimer = Math.max(num(target.fearTimer), 0.8); target.electricShockUntil = Math.max(num(target.electricShockUntil), now + 0.62); target.setActionState?.("panic", { target: null, reason: "\u611f\u96fb\u3057\u3066\u3044\u308b", wake: true, sleeping: false }); } emitEffect(worldRef, "electric_shock", target.x, target.y - Math.max(3, num(target.radius ?? target.r, 12) * 0.12), { size: Math.max(12, num(target.radius ?? target.r, 12) * 1.15), life: 0.28, color: "rgba(118,224,255,0.96)", }); global.TarinaiAchievements?.recordWireShock?.(wire, target, { world: worldRef, connected }); return true; } function damageFromWires(worldRef, wires) { const now = num(worldRef.time); for (const { wire, a, b } of wires) { global.TarinaiAchievements?.recordWirePowerState?.(wire, Boolean(wire.signalActive)); if (!wire.signalActive) continue; // Living endpoints are directly energized even through insulated wire. if (a && (a.radius || a.kind === "worker" || a.kind === "queen")) shockTarget(worldRef, wire, a, now, { connected: true }); if (b && b !== a && (b.radius || b.kind === "worker" || b.kind === "queen")) shockTarget(worldRef, wire, b, now, { connected: true }); // Only bare wire shocks creatures merely touching the span. if (wire.type !== "wire") continue; const pb = global.TarinaiPhysicsBodySystem; const pointA = endpointPoint(pb?.endpoint?.(wire, 0), worldRef); const pointB = endpointPoint(pb?.endpoint?.(wire, 1), worldRef); if (!pointA || !pointB) continue; const cx = (pointA.x + pointB.x) * 0.5, cy = (pointA.y + pointB.y) * 0.5; const radius = Math.hypot(pointB.x - pointA.x, pointB.y - pointA.y) * 0.5 + 42; const near = worldRef.nearbyTarinai?.(cx, cy, radius, true) || worldRef.tarinai || []; for (const t of near) { if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue; if (distanceToSegment(t.x, t.y, pointA.x, pointA.y, pointB.x, pointB.y) > Math.max(8, num(t.radius, 20) * 0.48)) continue; shockTarget(worldRef, wire, t, now, { connected: false }); } const ants = worldRef.nearbyAnts?.(cx, cy, radius, true) || worldRef.ants || []; for (const ant of ants) { if (!ant || ant.dead) continue; if (distanceToSegment(ant.x, ant.y, pointA.x, pointA.y, pointB.x, pointB.y) > Math.max(5, num(ant.r, 6) * 0.9)) continue; shockTarget(worldRef, wire, ant, now, { connected: false }); } } } function powerOverride(item) { if (!item || !item._signalDriven) return null; return Boolean(item._signalOn); } function updateWorld(worldRef, dt = 0.016) { if (!worldRef?.items) return false; updatePressureSwitches(worldRef); const wires = buildSignalNetwork(worldRef); damageFromWires(worldRef, wires); return true; } global.TarinaiSignalSystem = Object.freeze({ updateWorld, powerOverride, isWire, isMinuteInRange, currentMinuteOfDay, isSignalConnectable, DIRECT_SIGNAL_SINK_TYPES, SIGNAL_SOURCE_TYPES, SENSOR_METRICS: DETECTOR_METRICS, DETECTOR_METRICS, PRESSURE_TARGETS, detectorMetricValue, }); })(typeof window !== "undefined" ? window : globalThis);