248 lines
12 KiB
JavaScript
248 lines
12 KiB
JavaScript
"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 3;
|
|
return 5;
|
|
}
|
|
|
|
function nestBoxOccupants(worldRef, box, limit = 5) {
|
|
if (!worldRef || !box || box.dead || !isNestContainer(box)) return [];
|
|
// Each pipe mouth has its own local sleeping capacity. The underground
|
|
// network is still shared for exits, but occupancy/reservations must stay
|
|
// attached to the entrance pipe or every tarinai piles into the nearest one.
|
|
const occupants = (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 (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 nestOccupantPriority(tarinai) {
|
|
if (!tarinai) return -1;
|
|
if (tarinai.isTarinaiChampion) return 2;
|
|
if (tarinai.isZunchiSlave) return 0;
|
|
return 1;
|
|
}
|
|
|
|
function lowerPriorityNestOccupant(worldRef, box, entrant, occupants = null) {
|
|
const entrantPriority = nestOccupantPriority(entrant);
|
|
const list = Array.isArray(occupants) ? occupants : nestBoxOccupants(worldRef, box, Infinity);
|
|
let best = null;
|
|
let bestPriority = Infinity;
|
|
for (const occupant of list || []) {
|
|
if (!occupant || occupant.dead || occupant === entrant) continue;
|
|
const priority = nestOccupantPriority(occupant);
|
|
if (priority >= entrantPriority) continue;
|
|
if (priority < bestPriority) {
|
|
best = occupant;
|
|
bestPriority = priority;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function lowerPriorityNestReservation(worldRef, box, entrant) {
|
|
if (!worldRef || !box || !entrant) return null;
|
|
const now = Number(worldRef.time || 0) || 0;
|
|
const entrantPriority = nestOccupantPriority(entrant);
|
|
let best = null;
|
|
let bestPriority = Infinity;
|
|
for (const candidate of worldRef.tarinai || []) {
|
|
if (!candidate || candidate.dead || candidate === entrant) continue;
|
|
if (!tarinaiIsReservingNest(candidate, box, now)) continue;
|
|
const priority = nestOccupantPriority(candidate);
|
|
if (priority >= entrantPriority) continue;
|
|
if (priority < bestPriority) {
|
|
best = candidate;
|
|
bestPriority = priority;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
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.target === item || (tarinai.insideNestBoxId && tarinai.insideNestBoxId === item.id)) return true;
|
|
if (occupancy < capacity) return true;
|
|
if (!isNestContainer(item)) return false;
|
|
const occupants = nestBoxOccupants(worldRef, item, Infinity);
|
|
return Boolean(lowerPriorityNestOccupant(worldRef, item, tarinai, occupants) || lowerPriorityNestReservation(worldRef, item, tarinai));
|
|
}
|
|
|
|
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?.nearbyNonGrassItems?.(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 occupancy = bedOccupancy(worldRef, item);
|
|
const pipeLoadPenalty = type === "pipe" ? occupancy * 44 : 0;
|
|
const spread = type === "pipe" && typeof global.stableUnit === "function"
|
|
? global.stableUnit(tarinai?.id || tarinai?.familyKey || "tarinai", `pipe-choice:${item.id || "pipe"}`) * 14
|
|
: 0;
|
|
const score = d - comfort * 18 + pipeLoadPenalty + spread;
|
|
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,
|
|
nestOccupantPriority,
|
|
lowerPriorityNestOccupant,
|
|
});
|
|
})(typeof window !== "undefined" ? window : globalThis);
|