"use strict"; // Layer: tarinai/nest-sleep // Centralizes sleep furniture, nest/pipe occupancy/reservation, and sleep-target // selection policy so entrance logic and bed targeting do not drift. (function (global) { const distFn = typeof dist === "function" ? dist : ((a, b) => Math.hypot((a?.x || 0) - (b?.x || 0), (a?.y || 0) - (b?.y || 0))); const clampFn = typeof clamp === "function" ? clamp : ((v, a, b) => Math.max(a, Math.min(b, v))); function isNestContainer(item) { return Boolean(global.TarinaiToolRuntime?.isNestContainerItem?.(item, { alive: false })); } function isSleepFurniture(item) { return Boolean(item && (item.roles?.sleepPlace || (typeof global.isSleepFurnitureType === "function" ? global.isSleepFurnitureType(item.type) : (item.type === "bed" || item.type === "nest_box" || item.type === "pipe" || item.type === "grass_bed")))); } function sleepTargetBlocked(worldRef, tarinai, item) { if (!worldRef || !tarinai || !item) return false; // Pipe mouths are floor-level entry points. Treat them as reachable even if // their circular sprite overlaps the tarinai body. if (item.type === "pipe") return false; return Boolean(worldRef.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65))); } function pipeItems(worldRef) { return (worldRef?.items || []).filter(it => it && !it.dead && it.type === "pipe"); } function tarinaiInsidePipeNetwork(worldRef, t) { if (!worldRef || !t || t.dead || !t.insideNestBoxId) return false; const holder = typeof worldRef.nestBoxById === "function" ? worldRef.nestBoxById(t.insideNestBoxId) : (worldRef.items || []).find(it => it && !it.dead && it.id === t.insideNestBoxId); return holder?.type === "pipe"; } function nestBoxCapacity(worldRef, box = null) { if (typeof worldRef?.nestBoxCapacity === "function") return worldRef.nestBoxCapacity(box); if (box?.type === "pipe") return Math.max(3, pipeItems(worldRef).length * 3); return 5; } function nestBoxOccupants(worldRef, box, limit = 5) { if (!worldRef || !box || box.dead || !isNestContainer(box)) return []; const occupants = box.type === "pipe" ? (worldRef.tarinai || []).filter(t => tarinaiInsidePipeNetwork(worldRef, t)) : (worldRef.tarinai || []).filter(t => t && !t.dead && t.insideNestBoxId === box.id); return Number.isFinite(limit) ? occupants.slice(0, Math.max(0, limit)) : occupants; } function isTarinaiHiddenInNestBox(worldRef, t) { if (!t || t.dead || !t.insideNestBoxId) return false; if (typeof t.nestBoxEntryActive === "function" && t.nestBoxEntryActive()) return false; const now = Number(worldRef?.time ?? t.world?.time ?? 0) || 0; if (now < (t._nestBoxEntrySmoothUntil || -Infinity) || ((t.nestFade || 0) > 0.01 && (t.nestFade || 0) < 0.985)) return false; return true; } function tarinaiTargetContainer(t) { const target = t?.target || null; return isNestContainer(target) ? target : (isNestContainer(target?.shelterItem) ? target.shelterItem : (isNestContainer(target?.hostItem) ? target.hostItem : null)); } function tarinaiIsReservingNest(t, box, now = 0) { if (!t || t.dead || !box) return false; if (box.type === "pipe" && t.insideNestBoxId && tarinaiInsidePipeNetwork(t.world || null, t)) return false; if (box.type !== "pipe" && t.insideNestBoxId === box.id) return false; const target = tarinaiTargetContainer(t); if (box.type === "pipe") { if (target?.type !== "pipe") return false; } else if (target !== box) return false; if (t._nestBoxAvoidId === box.id && now < (t._nestBoxAvoidUntil || -Infinity)) return false; const seekingNest = t.state === "seek_bed" || t.state === "sleep" || t.sleeping || t.state === "seek_temperature" || t.behavior?.actionId === "sleep_in_bed"; if (!seekingNest) return false; return true; } function nestBoxReservationCount(worldRef, box) { if (!worldRef || !box || box.dead || !isNestContainer(box)) return 0; const now = Number(worldRef.time || 0) || 0; let n = 0; for (const t of worldRef.tarinai || []) if (tarinaiIsReservingNest(t, box, now)) n += 1; return n; } function bedOccupancy(worldRef, bed) { if (!worldRef || !isSleepFurniture(bed)) return 0; if (isNestContainer(bed)) { return nestBoxOccupants(worldRef, bed, Infinity).length + nestBoxReservationCount(worldRef, bed); } let n = 0; const radius = Math.max(86, (bed.r || 24) * 2.45); const near = typeof worldRef.nearbyTarinai === "function" ? worldRef.nearbyTarinai(bed.x, bed.y, radius) : (worldRef.tarinai || []); for (const t of near || []) { if (!t || t.dead || isTarinaiHiddenInNestBox(worldRef, t)) continue; if (t.target === bed && (t.state === "sleep" || t.state === "seek_bed" || t.sleeping)) n += 1; } return n; } function bedComfort(worldRef, bed) { if (!isSleepFurniture(bed)) return 0.7; const occ = bedOccupancy(worldRef, bed); const base = bed.type === "pipe" ? Math.min(Number(bed.comfort ?? 0.74) || 0.74, 0.82) : (bed.comfort ?? 1); return clampFn(base - Math.max(0, occ - 3) * 0.075 - (bed.wear || 0) * 0.18, bed.type === "pipe" ? 0.46 : 0.42, bed.type === "pipe" ? 0.88 : 1.18); } function sleepPlaceAvailableFor(worldRef, tarinai, item) { if (!worldRef || !tarinai || !item || item.dead || !isSleepFurniture(item)) return false; if (Number(item.unreachableUntil || 0) > Number(worldRef.time || 0)) return false; if (item.type === "nest_box" && globalThis.TarinaiItemDynamicToolSystem?.nestBoxFireActive?.(item)) return false; if (item.abandonedById && item.abandonedById === tarinai?.id && Number(item.abandonedUntil || 0) > Number(worldRef.time || 0)) return false; if (tarinai.shouldAvoidTarget?.(item)) return false; if (worldRef?.sleepTargetTemperatureBlocked?.(tarinai, item)) return false; if (sleepTargetBlocked(worldRef, tarinai, item)) return false; const capacity = isNestContainer(item) ? nestBoxCapacity(worldRef, item) : 1; const occupancy = bedOccupancy(worldRef, item); if (tarinai?.isTarinaiChampion && item.type === "nest_box") return true; return occupancy < capacity || tarinai.target === item || (tarinai.insideNestBoxId && tarinai.insideNestBoxId === item.id); } function findStructure(worldRef, tarinai, opts = {}) { const type = String(opts.type || ""); const maxDist = Number.isFinite(Number(opts.maxDist)) ? Number(opts.maxDist) : Infinity; const owned = opts.owned === true; const unowned = opts.unowned === true; const blocked = typeof opts.blocked === "function" ? opts.blocked : sleepTargetBlocked; let best = null, bestD = maxDist; for (const item of worldRef?.items || []) { if (!item?.isStructure || item.dead) continue; if (owned && item.ownerId !== tarinai?.id) continue; if (unowned && item.ownerId) continue; if (!owned && !unowned && opts.ownerId !== undefined && item.ownerId !== opts.ownerId) continue; if (Number(item.unreachableUntil || 0) > Number(worldRef?.time || 0)) continue; if (unowned && item.abandonedById && item.abandonedById === tarinai?.id && Number(item.abandonedUntil || 0) > Number(worldRef?.time || 0)) continue; if (type && item.type !== type) continue; if (unowned && tarinai?.shouldAvoidTarget?.(item)) continue; if (blocked(worldRef, tarinai, item)) continue; const d = distFn(tarinai, item); if (d < bestD) { best = item; bestD = d; } } return best; } function findOwnedStructure(worldRef, tarinai, type = "", maxDist = Infinity, opts = {}) { return findStructure(worldRef, tarinai, { ...opts, owned: true, type, maxDist }); } function findUnownedStructure(worldRef, tarinai, type = "", maxDist = Infinity, opts = {}) { return findStructure(worldRef, tarinai, { ...opts, unowned: true, type, maxDist }); } function nearestSleepPlaceOfType(worldRef, tarinai, type, maxDist = 520) { let best = null; let bestScore = Infinity; const list = worldRef?.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || worldRef?.items || []; for (const item of list) { if (!item || item.dead || item.type !== type) continue; if (!sleepPlaceAvailableFor(worldRef, tarinai, item)) continue; const d = distFn(tarinai, item); if (d > maxDist) continue; const comfort = bedComfort(worldRef, item); const score = d - comfort * 18; if (score < bestScore) { best = item; bestScore = score; } } return best; } function findPrioritySleepPlaceBeforeGrassBedBuild(worldRef, tarinai, maxDist = 520) { return findOwnedStructure(worldRef, tarinai, "grass_bed", maxDist) || nearestSleepPlaceOfType(worldRef, tarinai, "nest_box", maxDist) || nearestSleepPlaceOfType(worldRef, tarinai, "bed", maxDist) || nearestSleepPlaceOfType(worldRef, tarinai, "pipe", maxDist) || nearestSleepPlaceOfType(worldRef, tarinai, "grass_bed", maxDist) || null; } function findNearestSleepPlace(worldRef, tarinai, maxDist = 520) { return findPrioritySleepPlaceBeforeGrassBedBuild(worldRef, tarinai, maxDist) || null; } global.TarinaiNestSleepSystem = Object.freeze({ isSleepFurniture, isTarinaiHiddenInNestBox, nestBoxOccupants, bedOccupancy, bedComfort, sleepPlaceAvailableFor, findOwnedStructure, findUnownedStructure, nearestSleepPlaceOfType, findPrioritySleepPlaceBeforeGrassBedBuild, findNearestSleepPlace, }); })(typeof window !== "undefined" ? window : globalThis);