tarinai/js/tarinai_nest_sleep_system.js
2026-07-20 14:36:59 +09:00

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