2026-06-28 23:07:40 +09:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
// Layer: tarinai/nest-sleep
|
|
|
|
|
// Centralizes sleep furniture, nest occupancy/reservation, and sleep-target
|
|
|
|
|
// selection policy so nest-box entrance logic and bed targeting do not drift.
|
|
|
|
|
(function (global) {
|
2026-06-29 16:21:58 +09:00
|
|
|
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)));
|
2026-06-28 23:07:40 +09:00
|
|
|
|
|
|
|
|
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 === "grass_bed"))));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 21:02:04 +09:00
|
|
|
function sleepTargetBlocked(worldRef, tarinai, item) {
|
|
|
|
|
if (!worldRef || !tarinai || !item) return false;
|
|
|
|
|
return Boolean(worldRef.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65)));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 23:07:40 +09:00
|
|
|
function nestBoxCapacity(worldRef, box = null) {
|
2026-06-29 21:02:04 +09:00
|
|
|
return typeof worldRef?.nestBoxCapacity === "function" ? worldRef.nestBoxCapacity(box) : 5;
|
2026-06-28 23:07:40 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function nestBoxOccupants(worldRef, box, limit = 5) {
|
|
|
|
|
if (!worldRef || !box || box.dead || box.type !== "nest_box") return [];
|
|
|
|
|
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 tarinaiIsReservingNest(t, box, now = 0) {
|
|
|
|
|
if (!t || t.dead || !box || t.insideNestBoxId === box.id) return false;
|
|
|
|
|
if (t.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.behavior?.actionId === "sleep_in_bed";
|
|
|
|
|
if (!seekingNest) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function nestBoxReservationCount(worldRef, box) {
|
|
|
|
|
if (!worldRef || !box || box.dead || box.type !== "nest_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 (bed.type === "nest_box") {
|
|
|
|
|
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;
|
2026-07-01 16:55:49 +09:00
|
|
|
if (t.target === bed && (t.state === "sleep" || t.state === "seek_bed" || t.sleeping)) n += 1;
|
2026-06-28 23:07:40 +09:00
|
|
|
}
|
|
|
|
|
return n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bedComfort(worldRef, bed) {
|
|
|
|
|
if (!isSleepFurniture(bed)) return 0.7;
|
|
|
|
|
const occ = bedOccupancy(worldRef, bed);
|
|
|
|
|
return clampFn((bed.comfort ?? 1) - Math.max(0, occ - 3) * 0.075 - (bed.wear || 0) * 0.18, 0.42, 1.18);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sleepPlaceAvailableFor(worldRef, tarinai, item) {
|
|
|
|
|
if (!worldRef || !tarinai || !item || item.dead || !isSleepFurniture(item)) return false;
|
2026-06-30 10:57:06 +09:00
|
|
|
if (Number(item.unreachableUntil || 0) > Number(worldRef.time || 0)) return false;
|
|
|
|
|
if (item.abandonedById && item.abandonedById === tarinai?.id && Number(item.abandonedUntil || 0) > Number(worldRef.time || 0)) return false;
|
2026-06-28 23:07:40 +09:00
|
|
|
if (tarinai.shouldAvoidTarget?.(item)) return false;
|
2026-06-30 10:57:06 +09:00
|
|
|
if (worldRef?.sleepTargetTemperatureBlocked?.(tarinai, item)) return false;
|
2026-06-29 21:02:04 +09:00
|
|
|
if (sleepTargetBlocked(worldRef, tarinai, item)) return false;
|
2026-06-28 23:07:40 +09:00
|
|
|
const capacity = item.type === "nest_box" ? nestBoxCapacity(worldRef, item) : 1;
|
|
|
|
|
const occupancy = bedOccupancy(worldRef, item);
|
2026-07-01 16:55:49 +09:00
|
|
|
return occupancy < capacity || tarinai.target === item || (tarinai.insideNestBoxId && tarinai.insideNestBoxId === item.id);
|
2026-06-28 23:07:40 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-01 14:41:05 +09:00
|
|
|
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;
|
2026-06-28 23:07:40 +09:00
|
|
|
let best = null, bestD = maxDist;
|
|
|
|
|
for (const item of worldRef?.items || []) {
|
2026-07-01 14:41:05 +09:00
|
|
|
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;
|
2026-06-30 10:57:06 +09:00
|
|
|
if (Number(item.unreachableUntil || 0) > Number(worldRef?.time || 0)) continue;
|
2026-07-01 14:41:05 +09:00
|
|
|
if (unowned && item.abandonedById && item.abandonedById === tarinai?.id && Number(item.abandonedUntil || 0) > Number(worldRef?.time || 0)) continue;
|
2026-06-28 23:07:40 +09:00
|
|
|
if (type && item.type !== type) continue;
|
2026-07-01 14:41:05 +09:00
|
|
|
if (unowned && tarinai?.shouldAvoidTarget?.(item)) continue;
|
|
|
|
|
if (blocked(worldRef, tarinai, item)) continue;
|
2026-06-28 23:07:40 +09:00
|
|
|
const d = distFn(tarinai, item);
|
|
|
|
|
if (d < bestD) { best = item; bestD = d; }
|
|
|
|
|
}
|
|
|
|
|
return best;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 14:41:05 +09:00
|
|
|
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 });
|
2026-06-28 23:07:40 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
2026-07-01 16:55:49 +09:00
|
|
|
|| nearestSleepPlaceOfType(worldRef, tarinai, "grass_bed", maxDist)
|
2026-06-28 23:07:40 +09:00
|
|
|
|| null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findNearestSleepPlace(worldRef, tarinai, maxDist = 520) {
|
|
|
|
|
return findPrioritySleepPlaceBeforeGrassBedBuild(worldRef, tarinai, maxDist)
|
|
|
|
|
|| null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
global.TarinaiNestSleepSystem = Object.freeze({
|
|
|
|
|
isSleepFurniture,
|
|
|
|
|
isTarinaiHiddenInNestBox,
|
|
|
|
|
nestBoxOccupants,
|
|
|
|
|
bedOccupancy,
|
|
|
|
|
bedComfort,
|
|
|
|
|
sleepPlaceAvailableFor,
|
2026-07-01 14:41:05 +09:00
|
|
|
findStructure,
|
|
|
|
|
findOwnedStructure,
|
|
|
|
|
findUnownedStructure,
|
2026-06-28 23:07:40 +09:00
|
|
|
nearestSleepPlaceOfType,
|
|
|
|
|
findPrioritySleepPlaceBeforeGrassBedBuild,
|
|
|
|
|
findNearestSleepPlace,
|
|
|
|
|
});
|
|
|
|
|
})(typeof window !== "undefined" ? window : globalThis);
|