"use strict"; // Runtime behavior and configuration helpers for robot cleaner items. (function (global) { const ROBOT_DEFAULT_MASK = 0b000011; // zunchi + corpses and stains const ROBOT_MOVE_SPEED = 60; const ROBOT_HIGH_SPEED = 120; const ROBOT_RESCAN_INTERVAL = 2.0; const ROBOT_TARGET_BITS = Object.freeze({ zunchi: 1 << 0, ant_corpse: 1 << 1, tarinai: 1 << 2, grass: 1 << 3, grass_bed: 1 << 4, water: 1 << 5, }); const ROBOT_TARGET_ORDER = Object.freeze(["zunchi", "ant_corpse", "tarinai", "grass", "grass_bed", "water"]); function robotCleanerDefaultMask() { return ROBOT_DEFAULT_MASK; } function robotCleanerMask(item) { const raw = Number(item?.robotCleanerMask); return Number.isFinite(raw) ? (raw | 0) : ROBOT_DEFAULT_MASK; } function setRobotCleanerMask(item, mask = ROBOT_DEFAULT_MASK) { if (!item || item.type !== "robot_cleaner") return false; item.robotCleanerMask = Math.max(0, Math.min(0x3f, Number(mask) | 0)); return true; } function robotCleanerHighSpeed(item) { return Boolean(item?.robotCleanerHighSpeed); } function setRobotCleanerHighSpeed(item, enabled = false) { if (!item || item.type !== "robot_cleaner") return false; item.robotCleanerHighSpeed = Boolean(enabled); item.robotCleanerSpeed = robotCleanerEffectiveSpeed(item); return true; } function robotCleanerEffectiveSpeed(item) { return robotCleanerHighSpeed(item) ? ROBOT_HIGH_SPEED : ROBOT_MOVE_SPEED; } function robotCleanerHasTarget(item, key = "") { const bit = ROBOT_TARGET_BITS[String(key || "")]; return Boolean(bit && (robotCleanerMask(item) & bit)); } function robotTargetLabel(key = "") { switch (String(key || "")) { case "zunchi": return "\u305a\u3093\u3061"; case "ant_corpse": return "\u6b7b\u9ab8"; case "trace": return "\u6b7b\u9ab8"; case "splat": return "\u8840\u75d5"; case "tarinai": return "\u305f\u308a\u306a\u3044"; case "grass": return "\u8349"; case "grass_bed": return "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9"; case "water": return "\u6c34\u6ef4"; default: return "\u5bfe\u8c61"; } } function normalizeRobotCleanerState(item) { if (!item || item.type !== "robot_cleaner") return null; if (!Number.isFinite(Number(item.robotCleanerMask))) item.robotCleanerMask = ROBOT_DEFAULT_MASK; item.robotCleanerMask = Math.max(0, Math.min(0x3f, Number(item.robotCleanerMask) | 0)); item.amount = Math.max(Number(item.amount || 0) || 0, 999); if (!Number.isFinite(Number(item.robotCleanerScanTimer))) item.robotCleanerScanTimer = 0; if (!Number.isFinite(Number(item.robotCleanerCleanFlash))) item.robotCleanerCleanFlash = 0; if (!Number.isFinite(Number(item.robotCleanerAngle))) item.robotCleanerAngle = Number(item.angle || 0) || 0; if (!Number.isFinite(Number(item.robotCleanerWanderAngle))) item.robotCleanerWanderAngle = Number(item.robotCleanerAngle || 0) || 0; item.robotCleanerHighSpeed = Boolean(item.robotCleanerHighSpeed); item.robotCleanerSpeed = robotCleanerEffectiveSpeed(item); return item; } function robotTargetAlive(target) { if (!target) return false; return !target.dead && Number.isFinite(Number(target.x)) && Number.isFinite(Number(target.y)); } function robotTargetBlocked(item, target, worldRef) { if (!item || !target || !worldRef) return false; return Boolean(worldRef.targetBlockedByObstacle?.(item, target, Math.max(18, (item.r || 22) * 0.65))); } function currentRobotTarget(item, worldRef) { const key = String(item?.robotCleanerTargetType || ""); const id = String(item?.robotCleanerTargetId || ""); if (!key || !id || !worldRef) return null; if (key === "tarinai") { const t = worldRef.liveTarinaiById?.(id) || (worldRef.tarinai || []).find(t => t && t.id === id && !t.dead); return robotTargetAlive(t) && !robotTargetBlocked(item, t, worldRef) ? t : null; } const byId = typeof worldRef.itemById === "function" ? worldRef.itemById(id) : null; const found = byId || (worldRef.items || []).find(it => it && it.id === id && !it.dead); const matchesKey = found && (found.type === key || (key === "ant_corpse" && (found.type === "trace" || found.type === "splat"))); return matchesKey && robotTargetAlive(found) && !robotTargetBlocked(item, found, worldRef) ? found : null; } function robotTargetCandidates(item, worldRef) { const out = []; if (!worldRef || !item) return out; const mask = robotCleanerMask(item); worldRef.ensureItemBuckets?.("robot-cleaner-scan"); for (const key of ROBOT_TARGET_ORDER) { const bit = ROBOT_TARGET_BITS[key]; if (!(mask & bit)) continue; if (key === "tarinai") { for (const t of worldRef.tarinai || []) { if (!t || t.dead) continue; if (worldRef.isTarinaiHiddenInNestBox?.(t)) continue; if (robotTargetBlocked(item, t, worldRef)) continue; out.push({ target: t, type: key }); } continue; } const buckets = key === "ant_corpse" ? ["ant_corpse", "trace", "splat"].flatMap(type => worldRef.itemsOfType?.(type) || (worldRef.items || []).filter(it => it && it.type === type)) : (worldRef.itemsOfType?.(key) || (worldRef.items || []).filter(it => it && it.type === key)); for (const it of buckets || []) { if (!it || it.dead || it === item) continue; if (robotTargetBlocked(item, it, worldRef)) continue; out.push({ target: it, type: key }); } } return out; } function retargetRobotCleaner(item, worldRef, force = false) { const current = currentRobotTarget(item, worldRef); if (current && !force) return current; item.robotCleanerTargetId = ""; item.robotCleanerTargetType = ""; let best = null; let bestType = ""; let bestD = Infinity; for (const rec of robotTargetCandidates(item, worldRef)) { const target = rec.target; if (!robotTargetAlive(target)) continue; const d = distXY(item.x, item.y, target.x, target.y); if (d < bestD) { best = target; bestType = rec.type; bestD = d; } } if (best) { item.robotCleanerTargetId = best.id || ""; item.robotCleanerTargetType = bestType || best.type || ""; } return best; } function triggerRobotPanic(item, target, worldRef, force = false) { if (!item || !target || target.dead) return false; const now = Number(worldRef?.time || target.world?.time || 0) || 0; if (!force && now < Number(target.robotCleanerPanicCooldownUntil || 0)) return false; target.robotCleanerPanicCooldownUntil = now + 0.65; const reason = "\u30ed\u30dc\u6383\u9664\u6a5f\u304b\u3089\u9003\u3052\u3066\u3044\u308b"; if (typeof target.enterPanic === "function") { target.enterPanic({ threat: item, reason, fear: 0.86, wake: true, cause: "robot_cleaner", surpriseTimer: 0.18, awakeLockTimer: 0.45 }); } else { target.fearTimer = Math.max(target.fearTimer || 0, 0.86); target.setActionState?.("panic", { target: target.panicDestination ? target.panicDestination(item, true) : null, reason, wake: true }); } return true; } function isRobotTarinaiTarget(target) { return Boolean(target && (target.constructor?.name === "Tarinai" || (target.thought !== undefined && target.name && target.die))); } function robotTargetTypeForContact(item, target) { if (!item || !target || target.dead) return ""; if (isRobotTarinaiTarget(target)) return robotCleanerHasTarget(item, "tarinai") ? "tarinai" : ""; const type = String(target.type || ""); if ((type === "ant_corpse" || type === "trace" || type === "splat") && robotCleanerHasTarget(item, "ant_corpse")) return type; return robotCleanerHasTarget(item, type) ? type : ""; } function robotCleanerCaptureRadius(item, target) { return Math.max(18, (item?.r || 22) * 0.72 + (target?.r || target?.radius || 10) * 0.72); } function clearRobotTrackedTargetIfMatched(item, target) { if (!item || !target) return false; const id = String(target.id || ""); if (!id || String(item.robotCleanerTargetId || "") !== id) return false; item.robotCleanerTargetId = ""; item.robotCleanerTargetType = ""; item.robotCleanerScanTimer = 0; return true; } function cleanRobotTarget(item, target, worldRef, typeOverride = "") { if (!item || !target || !worldRef || target.dead) return false; const type = String(typeOverride || item.robotCleanerTargetType || target.type || ""); const label = robotTargetLabel(type); let removed = false; const cleanReason = "\u6383\u9664"; const cleanLogReason = "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u5438\u3044\u8fbc\u307e\u308c\u305f"; if (type === "tarinai" || isRobotTarinaiTarget(target)) { triggerRobotPanic(item, target, worldRef, true); if (typeof target.die === "function") { target.die(cleanReason, { lowHealth: true }); removed = true; } else { target.dead = true; target.deathReason = cleanReason; worldRef.markDead?.(target, cleanReason); removed = true; } } else { removed = Boolean(global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, target, { reason: "robot-cleaner", userReason: type === "grass_bed" ? "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u7247\u4ed8\u3051\u3089\u308c\u305f" : cleanLogReason, wake: true, breaker: item, })); if (!removed) { target.amount = 0; target.hp = 0; removed = true; } } if (!removed) return false; clearRobotTrackedTargetIfMatched(item, target); item.robotCleanerScanTimer = 0; item.robotCleanerCleanFlash = 0.42; item.robotCleanerCleanCount = (Number(item.robotCleanerCleanCount || 0) || 0) + 1; global.TarinaiAchievements?.recordRobotClean?.({ world: worldRef, robot: item, target, type }); worldRef.effects?.push(new Effect("ring", target.x, target.y, { size: Math.max(16, (target.r || target.radius || 14) * 1.8), life: 0.24, color: "rgba(146,206,220,0.36)" })); worldRef.markItemBucketsDirty?.("robot-cleaner"); worldRef.markSpatialDirty?.("robot-cleaner"); if (target.type === "trace" || target.type === "splat") worldRef.markTerrainDirtyAt?.(target.x, target.y, Math.max(target.r || target.radius || 24, 36), "robot-cleaner"); worldRef.updateItemCounts?.(); worldRef.drawListDirty = true; if ((worldRef.time || 0) - Number(item.robotCleanerLastLogAt || -999) > 1.2) { worldRef.log?.(`\u30ed\u30dc\u6383\u9664\u6a5f\u304c${label}\u3092\u7247\u4ed8\u3051\u305f\u3002`, "observe"); item.robotCleanerLastLogAt = worldRef.time || 0; } return true; } function cleanRobotContacts(item, worldRef) { if (!item || !worldRef) return false; const seen = new Set(); let changed = false; for (const rec of robotTargetCandidates(item, worldRef)) { const target = rec?.target; if (!robotTargetAlive(target)) continue; const key = String(target.id || `${rec.type}:${target.x}:${target.y}`); if (seen.has(key)) continue; seen.add(key); const type = robotTargetTypeForContact(item, target) || rec.type || ""; if (!type) continue; const capture = robotCleanerCaptureRadius(item, target); if (distXY(item.x, item.y, target.x, target.y) <= capture) { if (cleanRobotTarget(item, target, worldRef, type)) changed = true; } } return changed; } function moveRobotCleaner(item, dt, target, worldRef) { const r = Math.max(14, Number(item.r || 22) || 22); const speed = robotCleanerEffectiveSpeed(item); let desiredAngle = Number(item.robotCleanerWanderAngle || 0) || 0; let desiredSpeed = speed; if (target && robotTargetAlive(target)) { desiredAngle = Math.atan2((target.y || 0) - (item.y || 0), (target.x || 0) - (item.x || 0)); desiredSpeed = speed; } else if ((item.robotCleanerWanderTimer || 0) <= 0) { desiredAngle = global.deterministicAngle(worldRef, "robot-cleaner-wander", item, Math.floor((worldRef.time || 0) * 2)); item.robotCleanerWanderTimer = global.deterministicRange(worldRef, "robot-cleaner-wander-timer", 1.2, 3.2, item); } item.robotCleanerWanderTimer = Math.max(0, Number(item.robotCleanerWanderTimer || 0) - dt); item.robotCleanerWanderAngle = desiredAngle; const vx = Math.cos(desiredAngle) * desiredSpeed; const vy = Math.sin(desiredAngle) * desiredSpeed; const blend = Math.min(1, dt * 6.2); item.vx = (Number(item.vx || 0) || 0) + (vx - (Number(item.vx || 0) || 0)) * blend; item.vy = (Number(item.vy || 0) || 0) + (vy - (Number(item.vy || 0) || 0)) * blend; item.prevX = item.x; item.prevY = item.y; item.x += item.vx * dt; item.y += item.vy * dt; const minX = CONFIG.worldPadding + r * 0.5; const minY = CONFIG.worldPadding + r * 0.5; const maxX = Math.max(minX, (worldRef.w || 0) - CONFIG.worldPadding - r * 0.5); const maxY = Math.max(minY, (worldRef.h || 0) - CONFIG.worldPadding - r * 0.5); let bounced = false; if (item.x < minX) { item.x = minX; item.vx = Math.abs(item.vx) * 0.35; bounced = true; } if (item.x > maxX) { item.x = maxX; item.vx = -Math.abs(item.vx) * 0.35; bounced = true; } if (item.y < minY) { item.y = minY; item.vy = Math.abs(item.vy) * 0.35; bounced = true; } if (item.y > maxY) { item.y = maxY; item.vy = -Math.abs(item.vy) * 0.35; bounced = true; } if (bounced) { item.robotCleanerWanderAngle = Math.atan2(item.vy || 0, item.vx || 0) + global.deterministicRange(worldRef, "robot-cleaner-bounce", -0.8, 0.8, item); item.robotCleanerWanderTimer = 0.4; } if (Math.hypot(item.vx || 0, item.vy || 0) > 2) item.robotCleanerAngle = Math.atan2(item.vy || 0, item.vx || 0); worldRef.markSpatialDirty?.("robot-cleaner-move"); worldRef.drawListDirty = true; } function updateRobotCleaner(item, dt, worldRef) { if (!item || item.dead || !worldRef) return false; normalizeRobotCleanerState(item); dt = Math.max(0, Math.min(0.05, Number(dt || 0) || 0)); // A connected signal wire becomes the cleaner's power source. Unwired // cleaners keep their original always-on behavior. if (item._signalDriven && !item._signalOn) { item.vx = 0; item.vy = 0; item.robotCleanerTargetId = ""; item.robotCleanerTargetType = ""; item.robotCleanerScanTimer = 0; worldRef.drawListDirty = true; return true; } item.robotCleanerCleanFlash = Math.max(0, Number(item.robotCleanerCleanFlash || 0) - dt * 2.8); item.robotCleanerScanTimer = Math.max(0, Number(item.robotCleanerScanTimer || 0) - dt); let target = currentRobotTarget(item, worldRef); if (!target || item.robotCleanerScanTimer <= 0) { target = retargetRobotCleaner(item, worldRef, true); item.robotCleanerScanTimer = ROBOT_RESCAN_INTERVAL; } moveRobotCleaner(item, dt, target, worldRef); if (target && robotTargetAlive(target)) { const isTarinaiTarget = String(item.robotCleanerTargetType || "") === "tarinai" || isRobotTarinaiTarget(target); const d = distXY(item.x, item.y, target.x, target.y); if (isTarinaiTarget && d <= Math.max(96, (item.r || 22) + (target.radius || target.r || 12) + 70)) triggerRobotPanic(item, target, worldRef); } cleanRobotContacts(item, worldRef); return true; } global.TarinaiRobotCleanerSystem = Object.freeze({ updateRobotCleaner, robotCleanerDefaultMask, robotCleanerMask, setRobotCleanerMask, robotCleanerHasTarget, robotCleanerHighSpeed, setRobotCleanerHighSpeed, robotCleanerEffectiveSpeed, robotTargetLabel, robotCleanerTargetBits: ROBOT_TARGET_BITS, robotCleanerTargetOrder: ROBOT_TARGET_ORDER, robotCleanerMoveSpeed: ROBOT_MOVE_SPEED, robotCleanerHighMoveSpeed: ROBOT_HIGH_SPEED, }); })(typeof window !== "undefined" ? window : globalThis);