2026-07-01 14:41:05 +09:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
// Layer: world/temperature
|
|
|
|
|
// Temperature, seasons, climate comfort scoring, and temperature-driven target selection.
|
|
|
|
|
(function (global) {
|
|
|
|
|
const World = global.World;
|
|
|
|
|
if (!World) return;
|
|
|
|
|
|
|
|
|
|
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
|
|
|
|
temperatureNumber(value, fallback = CONFIG.standardTemperature ?? 15) {
|
|
|
|
|
const v = Number(value);
|
|
|
|
|
const fb = Number(fallback);
|
|
|
|
|
return Number.isFinite(v) ? v : (Number.isFinite(fb) ? fb : 15);
|
|
|
|
|
},
|
|
|
|
|
climateTemperature(value) {
|
|
|
|
|
// \u81EA\u7136\u6C17\u5019\u3060\u3051\u306F\u4ED5\u69D8\u3069\u304A\u308A -20\u2103\u301C50\u2103 \u306E\u7BC4\u56F2\u306B\u53CE\u3081\u308B\u3002
|
|
|
|
|
const v = this.temperatureNumber(value);
|
|
|
|
|
return clamp(v, CONFIG.climateTemperatureMin ?? -20, CONFIG.climateTemperatureMax ?? 50);
|
|
|
|
|
},
|
|
|
|
|
seasonIndex() {
|
|
|
|
|
return Math.floor((((Number(this.day) || 1) - 1) % 20) / 5);
|
|
|
|
|
},
|
|
|
|
|
seasonLabel() {
|
|
|
|
|
return ["\u6625", "\u590F", "\u79CB", "\u51AC"][this.seasonIndex()] || "\u6625";
|
|
|
|
|
},
|
|
|
|
|
seasonDay() {
|
|
|
|
|
return ((((Number(this.day) || 1) - 1) % 5) + 1);
|
|
|
|
|
},
|
|
|
|
|
seasonDayString() {
|
|
|
|
|
return `${this.seasonLabel()}${this.seasonDay()}\u65E5`;
|
|
|
|
|
},
|
|
|
|
|
temperatureStableUnit(salt = "") {
|
|
|
|
|
const key = `${this.worldSeed || "world"}:temperature:${salt}`;
|
|
|
|
|
if (typeof stableUnit === "function") return stableUnit(key, "temperature");
|
|
|
|
|
let h = 2166136261;
|
|
|
|
|
for (let i = 0; i < key.length; i++) h = Math.imul(h ^ key.charCodeAt(i), 16777619);
|
|
|
|
|
return ((h >>> 0) % 1000000) / 1000000;
|
|
|
|
|
},
|
|
|
|
|
dailyTemperatureRange(day = this.day, weather = this.weather) {
|
|
|
|
|
const raw = this.temperatureStableUnit(`daily-range:${day}`) * 15;
|
|
|
|
|
if (weather === "sunny") return clamp(6 + raw * 0.62, 0, 15);
|
|
|
|
|
if (weather === "light_rain") return clamp(raw * 0.32, 0, 5);
|
|
|
|
|
return clamp(raw * 0.45, 0, 7);
|
|
|
|
|
},
|
|
|
|
|
annualTemperatureOffset() {
|
|
|
|
|
const dayLength = Math.max(1, CONFIG.dayLength || 120);
|
|
|
|
|
const totalDays = Math.max(0, Number(this.time || 0) / dayLength);
|
|
|
|
|
const phase = ((totalDays % 20) + 20) % 20 / 20;
|
|
|
|
|
const range = Number.isFinite(Number(this.temperatureAnnualRange)) ? Number(this.temperatureAnnualRange) : 30;
|
|
|
|
|
const wave = Math.sin(phase * Math.PI * 2 - Math.PI * 0.25);
|
|
|
|
|
return wave * range * 0.5;
|
|
|
|
|
},
|
2026-07-05 18:01:36 +09:00
|
|
|
seasonTemperatureBias() {
|
|
|
|
|
return this.seasonIndex?.() === 1 ? (CONFIG.summerTemperatureBoost ?? 5) : 0;
|
|
|
|
|
},
|
2026-07-01 14:41:05 +09:00
|
|
|
dailyTemperatureWave(progress = 0) {
|
|
|
|
|
const hour = ((((Number(progress) || 0) % 1) + 1) % 1) * 24;
|
|
|
|
|
const minHour = 5.0; // \u671D\u65B9\u306B\u6700\u4F4E\u6C17\u6E29
|
|
|
|
|
const maxHour = 14.0; // \u663C\u904E\u304E\u306B\u6700\u9AD8\u6C17\u6E29
|
|
|
|
|
const smooth = (x) => {
|
|
|
|
|
const t = clamp(x, 0, 1);
|
|
|
|
|
return t * t * (3 - 2 * t);
|
|
|
|
|
};
|
|
|
|
|
let warmth;
|
|
|
|
|
if (hour >= minHour && hour <= maxHour) {
|
|
|
|
|
warmth = smooth((hour - minHour) / Math.max(0.001, maxHour - minHour));
|
|
|
|
|
} else {
|
|
|
|
|
const downHour = hour > maxHour ? hour - maxHour : hour + 24 - maxHour;
|
|
|
|
|
const downSpan = minHour + 24 - maxHour;
|
|
|
|
|
warmth = 1 - smooth(downHour / Math.max(0.001, downSpan));
|
|
|
|
|
}
|
|
|
|
|
return warmth * 2 - 1;
|
|
|
|
|
},
|
|
|
|
|
naturalTemperature() {
|
|
|
|
|
// \u521D\u671F\u30ED\u30FC\u30C9\u76F4\u5F8C\u306F 10\u2103 \u3092\u4FDD\u6301\u3057\u3001\u521D\u56DE\u30D5\u30EC\u30FC\u30E0\u306Edt\u63FA\u308C\u30670\u2103\u8FD1\u304F\u3078\u843D\u3061\u306A\u3044\u3088\u3046\u306B\u3059\u308B\u3002
|
|
|
|
|
if ((Number(this.time) || 0) <= 6.0 && (Number(this.day) || 1) === 1) {
|
|
|
|
|
this.temperatureDailyRange = 0;
|
|
|
|
|
return 10;
|
|
|
|
|
}
|
|
|
|
|
const dayLength = Math.max(1, CONFIG.dayLength || 120);
|
|
|
|
|
const progress = ((Number(this.time || 0) % dayLength) + dayLength) % dayLength / dayLength;
|
|
|
|
|
const dailyRange = this.dailyTemperatureRange(this.day, this.weather);
|
|
|
|
|
const dailyWave = this.dailyTemperatureWave ? this.dailyTemperatureWave(progress) : Math.sin(progress * Math.PI * 2 - Math.PI * 0.5);
|
|
|
|
|
let temp;
|
|
|
|
|
if ((Number(this.day) || 1) === 1) {
|
|
|
|
|
// \u66251\u65E5\u76EE\u306F\u958B\u59CB\u664210\u2103\u3092\u57FA\u6E96\u306B\u3057\u3064\u3064\u3001\u65E5\u5185\u306E\u6700\u4F4E/\u6700\u9AD8\u6642\u523B\u3060\u3051\u306F\u7DAD\u6301\u3059\u308B\u3002
|
|
|
|
|
const initialWave = this.dailyTemperatureWave ? this.dailyTemperatureWave(0) : Math.sin(-Math.PI * 0.5);
|
|
|
|
|
temp = 10 + (dailyWave - initialWave) * dailyRange * 0.5;
|
|
|
|
|
} else {
|
|
|
|
|
temp = (CONFIG.standardTemperature ?? 15) + this.annualTemperatureOffset() + dailyWave * dailyRange * 0.5;
|
|
|
|
|
}
|
2026-07-05 18:01:36 +09:00
|
|
|
temp += this.seasonTemperatureBias?.() ?? 0;
|
2026-07-01 14:41:05 +09:00
|
|
|
this.temperatureDailyRange = dailyRange;
|
|
|
|
|
return this.climateTemperature(temp);
|
|
|
|
|
},
|
|
|
|
|
updateTemperature(dt = 0) {
|
|
|
|
|
if (!Number.isFinite(Number(this.temperatureAnnualRange))) {
|
|
|
|
|
this.temperatureAnnualRange = 25 + this.temperatureStableUnit("annual-range") * 10;
|
|
|
|
|
}
|
|
|
|
|
if (!Number.isFinite(Number(this.manualTemperature))) this.manualTemperature = CONFIG.standardTemperature ?? 15;
|
|
|
|
|
this.currentTemperature = this.temperatureAuto === false
|
|
|
|
|
? this.temperatureNumber(this.manualTemperature)
|
|
|
|
|
: this.naturalTemperature();
|
|
|
|
|
return this.currentTemperature;
|
|
|
|
|
},
|
|
|
|
|
setTemperatureAuto(enabled = true) {
|
|
|
|
|
this.temperatureAuto = Boolean(enabled);
|
|
|
|
|
if (this.temperatureAuto === false) this.manualTemperature = this.temperatureNumber(this.currentTemperature ?? this.manualTemperature ?? CONFIG.standardTemperature ?? 15);
|
|
|
|
|
return this.updateTemperature?.(0) ?? this.currentTemperature;
|
|
|
|
|
},
|
|
|
|
|
setManualTemperature(value = 15) {
|
|
|
|
|
this.manualTemperature = this.temperatureNumber(value);
|
|
|
|
|
if (this.temperatureAuto === false) this.currentTemperature = this.manualTemperature;
|
|
|
|
|
return this.manualTemperature;
|
|
|
|
|
},
|
2026-07-11 21:13:39 +09:00
|
|
|
weatherFeltTemperatureOffsetAt(x, y, actor = null) {
|
|
|
|
|
if (!actor) return 0;
|
|
|
|
|
const weather = String(this.weather || "sunny");
|
|
|
|
|
if (weather === "sunny") return 3.5;
|
|
|
|
|
if (weather === "light_rain") return -3.5;
|
|
|
|
|
return 0;
|
|
|
|
|
},
|
2026-07-01 14:41:05 +09:00
|
|
|
temperatureAt(x, y, actor = null) {
|
|
|
|
|
let temp = Number.isFinite(Number(this.currentTemperature)) ? Number(this.currentTemperature) : this.updateTemperature?.(0) ?? (CONFIG.standardTemperature ?? 15);
|
|
|
|
|
if (this.temperatureAuto === false) temp = this.temperatureNumber(this.manualTemperature);
|
|
|
|
|
if ((this.groundType || "soil") === "ice") temp -= 5;
|
|
|
|
|
const range = Math.max(24, CONFIG.temperatureItemRange ?? 190);
|
2026-07-11 21:13:39 +09:00
|
|
|
const fanRange = 320;
|
|
|
|
|
for (const it of this.nearbyItems(x, y, Math.max(range + 8, fanRange + 40))) {
|
2026-07-03 00:45:22 +09:00
|
|
|
if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove" && it.type !== "fan")) continue;
|
|
|
|
|
if (it.type === "fan") {
|
2026-07-11 21:13:39 +09:00
|
|
|
const wind = global.TarinaiItemDynamicBallSystem?.fanWindAt?.(it, x, y, { range: fanRange }) || null;
|
|
|
|
|
if (!wind) continue;
|
|
|
|
|
temp -= 5 * wind.falloff;
|
2026-07-03 00:45:22 +09:00
|
|
|
continue;
|
|
|
|
|
}
|
2026-07-01 14:41:05 +09:00
|
|
|
const d = distXY(x, y, it.x, it.y);
|
|
|
|
|
if (d > range) continue;
|
|
|
|
|
const edge = d <= range - 24 ? 1 : clamp((range - d) / 24, 0, 1);
|
|
|
|
|
temp += (it.type === "stove" ? 10 : -10) * edge;
|
|
|
|
|
}
|
|
|
|
|
const nestWarmthLimit = 5;
|
|
|
|
|
for (const box of this.nearbyItems?.(x, y, 86) || []) {
|
|
|
|
|
if (!box || box.dead || box.type !== "nest_box") continue;
|
|
|
|
|
const r = Math.max(36, Number(box.r || 42) || 42);
|
|
|
|
|
const base = this.nestBoxBaseRect?.(box) || null;
|
|
|
|
|
const inBase = base && x >= base.left - 4 && x <= base.right + 4 && y >= base.top - 4 && y <= base.bottom + 4;
|
|
|
|
|
const inBody = distXY(x, y, box.x, box.y) <= r * 1.08;
|
|
|
|
|
if (!inBase && !inBody) continue;
|
|
|
|
|
const toward = 20 - temp;
|
|
|
|
|
temp += clamp(toward, -nestWarmthLimit, nestWarmthLimit);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-07-11 21:13:39 +09:00
|
|
|
const parasolSystem = global.TarinaiParasolSystem || global.TarinaiRainShelterSystem;
|
|
|
|
|
const weatherBlocked = Boolean(actor && parasolSystem?.blocksWeatherAt?.(this, x, y));
|
|
|
|
|
temp += weatherBlocked ? 0 : (this.weatherFeltTemperatureOffsetAt?.(x, y, actor) || 0);
|
|
|
|
|
if (actor && !weatherBlocked && (actor.state === "sunbath" || (actor.sunbathTimer || 0) > 0.04)) temp += 3;
|
2026-07-01 14:41:05 +09:00
|
|
|
return this.temperatureNumber(temp);
|
|
|
|
|
},
|
2026-07-03 00:45:22 +09:00
|
|
|
pipeInteriorTemperatureFor(pipe, actor = null) {
|
|
|
|
|
if (!pipe || pipe.dead || pipe.type !== "pipe") return null;
|
|
|
|
|
const base = this.temperatureAt?.(pipe.x, pipe.y, null);
|
|
|
|
|
const temp = this.temperatureNumber(base, this.currentTemperature ?? CONFIG.standardTemperature ?? 15);
|
|
|
|
|
const toward = 20 - temp;
|
2026-07-03 20:57:02 +09:00
|
|
|
return this.temperatureNumber(temp + clamp(toward, -5, 5));
|
2026-07-03 00:45:22 +09:00
|
|
|
},
|
2026-07-01 14:41:05 +09:00
|
|
|
feltTemperatureFor(actor, x = actor?.x, y = actor?.y) {
|
|
|
|
|
if (!actor) return this.temperatureAt(x, y);
|
2026-07-03 00:45:22 +09:00
|
|
|
if (actor.insideNestBoxId && this.nestBoxById) {
|
|
|
|
|
const box = this.nestBoxById(actor.insideNestBoxId);
|
|
|
|
|
if (box?.type === "pipe") {
|
|
|
|
|
const pipeTemp = this.pipeInteriorTemperatureFor?.(box, actor);
|
|
|
|
|
if (Number.isFinite(Number(pipeTemp))) return pipeTemp;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-01 14:41:05 +09:00
|
|
|
return this.temperatureAt(x, y, actor);
|
|
|
|
|
},
|
|
|
|
|
temperatureComfortOffsetFor(actor = null) {
|
|
|
|
|
const raw = actor?.genetics?.temperatureOffset;
|
|
|
|
|
const n = Number(raw);
|
|
|
|
|
return Number.isFinite(n) ? n : 0;
|
|
|
|
|
},
|
|
|
|
|
temperatureStatusFor(temp = CONFIG.standardTemperature ?? 15, actor = null) {
|
|
|
|
|
const value = this.temperatureNumber(temp);
|
|
|
|
|
const offset = this.temperatureComfortOffsetFor(actor);
|
|
|
|
|
const min = (CONFIG.temperatureComfortMin ?? 10) + offset;
|
|
|
|
|
const max = (CONFIG.temperatureComfortMax ?? 25) + offset;
|
|
|
|
|
const discomfort = value < min ? min - value : (value > max ? value - max : 0);
|
|
|
|
|
const direction = value < min ? "cold" : (value > max ? "hot" : "comfort");
|
|
|
|
|
const stressExcess = Math.max(0, discomfort - (CONFIG.temperatureStressMargin ?? 5));
|
|
|
|
|
const harmExcess = Math.max(0, discomfort - (CONFIG.temperatureEnergyMargin ?? 10));
|
|
|
|
|
return {
|
|
|
|
|
temp: value,
|
|
|
|
|
direction,
|
|
|
|
|
discomfort,
|
|
|
|
|
comfortable: discomfort <= 0,
|
|
|
|
|
stressExcess,
|
|
|
|
|
harmExcess,
|
|
|
|
|
harmful: harmExcess > 0,
|
|
|
|
|
label: discomfort <= 0 ? "\u5FEB\u9069" : (direction === "cold" ? (harmExcess > 0 ? "\u5371\u967A\u306A\u5BD2\u3055" : "\u5BD2\u3044") : (harmExcess > 0 ? "\u5371\u967A\u306A\u6691\u3055" : "\u6691\u3044")),
|
|
|
|
|
};
|
|
|
|
|
},
|
2026-07-05 18:01:36 +09:00
|
|
|
temperatureComfortCandidateItems(direction = "") {
|
|
|
|
|
const bucketed = typeof this.itemsOfType === "function";
|
|
|
|
|
if (!bucketed) return this.items || [];
|
|
|
|
|
const types = ["nest_box", "pipe"];
|
|
|
|
|
if (direction === "cold") types.push("stove");
|
|
|
|
|
else if (direction === "hot") types.push("dry_ice", "fan");
|
|
|
|
|
else types.push("stove", "dry_ice", "fan");
|
2026-07-11 21:13:39 +09:00
|
|
|
const weather = String(this.weather || "sunny");
|
|
|
|
|
if ((direction === "hot" && weather === "sunny") || (direction === "cold" && weather === "light_rain")) types.push("rain_shelter");
|
2026-07-05 18:01:36 +09:00
|
|
|
const seen = new Set();
|
|
|
|
|
const out = [];
|
|
|
|
|
for (const type of types) {
|
|
|
|
|
for (const it of this.itemsOfType(type) || []) {
|
|
|
|
|
if (!it || it.dead || seen.has(it)) continue;
|
|
|
|
|
seen.add(it);
|
|
|
|
|
out.push(it);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
},
|
2026-07-01 14:41:05 +09:00
|
|
|
findComfortTemperatureSpot(actor, opts = {}) {
|
|
|
|
|
if (!actor || actor.dead) return null;
|
|
|
|
|
const current = this.feltTemperatureFor?.(actor) ?? this.temperatureAt(actor.x, actor.y, actor);
|
|
|
|
|
const currentStatus = this.temperatureStatusFor(current, actor);
|
|
|
|
|
if (!opts.force && currentStatus.comfortable) return null;
|
|
|
|
|
const comfortOffset = this.temperatureComfortOffsetFor(actor);
|
|
|
|
|
const targetTemp = ((CONFIG.temperatureComfortMin ?? 10) + (CONFIG.temperatureComfortMax ?? 25)) * 0.5 + comfortOffset;
|
|
|
|
|
const radius = actor.radius || actor.r || 20;
|
|
|
|
|
const rings = [72, 128, 196, 288, 392];
|
|
|
|
|
const dirs = 18;
|
|
|
|
|
const candidates = [];
|
2026-07-03 20:57:02 +09:00
|
|
|
const pushCandidate = (x, y, extra = 0, targetItem = null, tempOverride = null) => candidates.push({
|
2026-07-01 14:41:05 +09:00
|
|
|
x: clamp(x, radius + 8, (this.w || 1000) - radius - 8),
|
|
|
|
|
y: clamp(y, radius + 8, (this.h || 720) - radius - 8),
|
|
|
|
|
extra,
|
2026-07-03 20:57:02 +09:00
|
|
|
targetItem,
|
|
|
|
|
tempOverride,
|
2026-07-01 14:41:05 +09:00
|
|
|
});
|
|
|
|
|
for (const ring of rings) {
|
|
|
|
|
for (let i = 0; i < dirs; i++) {
|
|
|
|
|
const a = (Math.PI * 2 * i / dirs) + ring * 0.017;
|
|
|
|
|
pushCandidate(actor.x + Math.cos(a) * ring, actor.y + Math.sin(a) * ring);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const itemRange = Math.max(24, CONFIG.temperatureItemRange ?? 190);
|
2026-07-05 18:01:36 +09:00
|
|
|
for (const it of this.temperatureComfortCandidateItems?.(currentStatus.direction) || this.items || []) {
|
2026-07-03 20:57:02 +09:00
|
|
|
if (!it || it.dead) continue;
|
2026-07-11 21:13:39 +09:00
|
|
|
if (it.type === "rain_shelter") {
|
|
|
|
|
const parasolSystem = global.TarinaiParasolSystem || global.TarinaiRainShelterSystem;
|
|
|
|
|
const footprint = parasolSystem?.parasolFootprint?.(it) || {
|
|
|
|
|
centerX: it.x,
|
|
|
|
|
centerY: it.y + (Number(it.r || 50) || 50) * 0.84,
|
|
|
|
|
radiusX: (Number(it.r || 50) || 50) * 1.58,
|
|
|
|
|
radiusY: (Number(it.r || 50) || 50) * 0.68,
|
|
|
|
|
};
|
|
|
|
|
for (const [nx, ny] of [[0.82, 0], [-0.82, 0], [0.72, 0.36], [-0.72, 0.36], [0.72, -0.36], [-0.72, -0.36]]) {
|
|
|
|
|
pushCandidate(footprint.centerX + footprint.radiusX * nx, footprint.centerY + footprint.radiusY * ny, -72);
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-07-03 20:57:02 +09:00
|
|
|
if (it.type === "nest_box" || it.type === "pipe") {
|
|
|
|
|
const shelterTemp = it.type === "pipe" && this.pipeInteriorTemperatureFor
|
|
|
|
|
? this.pipeInteriorTemperatureFor(it, actor)
|
|
|
|
|
: this.temperatureAt?.(it.x, it.y, actor);
|
|
|
|
|
if (!Number.isFinite(Number(shelterTemp))) continue;
|
|
|
|
|
const shelterStatus = this.temperatureStatusFor(shelterTemp, actor);
|
|
|
|
|
if (currentStatus.direction === "cold" && shelterTemp <= current + 0.6 && !shelterStatus.comfortable) continue;
|
|
|
|
|
if (currentStatus.direction === "hot" && shelterTemp >= current - 0.6 && !shelterStatus.comfortable) continue;
|
|
|
|
|
const entry = this.nestBoxEntryPoint?.(it) || { x: it.x, y: it.y };
|
|
|
|
|
pushCandidate(entry.x, entry.y, shelterStatus.comfortable ? -96 : -44, it, shelterTemp);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (it.type !== "dry_ice" && it.type !== "stove" && it.type !== "fan") continue;
|
2026-07-01 14:41:05 +09:00
|
|
|
if (currentStatus.direction === "cold" && it.type !== "stove") continue;
|
2026-07-03 00:45:22 +09:00
|
|
|
if (currentStatus.direction === "hot" && it.type !== "dry_ice" && it.type !== "fan") continue;
|
|
|
|
|
if (it.type === "fan") {
|
|
|
|
|
const a = itemAngleFor(it);
|
|
|
|
|
pushCandidate(it.x + Math.cos(a) * itemRange * 0.52, it.y + Math.sin(a) * itemRange * 0.52, -50);
|
|
|
|
|
pushCandidate(it.x + Math.cos(a + 0.48) * itemRange * 0.72, it.y + Math.sin(a + 0.48) * itemRange * 0.72, -28);
|
|
|
|
|
pushCandidate(it.x + Math.cos(a - 0.48) * itemRange * 0.72, it.y + Math.sin(a - 0.48) * itemRange * 0.72, -28);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-07-01 14:41:05 +09:00
|
|
|
const toward = Math.atan2(actor.y - it.y, actor.x - it.x);
|
|
|
|
|
pushCandidate(it.x + Math.cos(toward) * itemRange * 0.45, it.y + Math.sin(toward) * itemRange * 0.45, -60);
|
|
|
|
|
pushCandidate(it.x + Math.cos(toward + 0.75) * itemRange * 0.62, it.y + Math.sin(toward + 0.75) * itemRange * 0.62, -32);
|
|
|
|
|
pushCandidate(it.x + Math.cos(toward - 0.75) * itemRange * 0.62, it.y + Math.sin(toward - 0.75) * itemRange * 0.62, -32);
|
|
|
|
|
}
|
|
|
|
|
let best = null;
|
|
|
|
|
let bestScore = Infinity;
|
|
|
|
|
for (const c of candidates) {
|
|
|
|
|
if (this.pointBlockedByObstacle?.(c.x, c.y, Math.max(14, radius * 0.72), { exclude: actor })) continue;
|
2026-07-11 21:13:39 +09:00
|
|
|
const temp = c.tempOverride != null && Number.isFinite(Number(c.tempOverride)) ? Number(c.tempOverride) : this.temperatureAt(c.x, c.y, actor);
|
2026-07-01 14:41:05 +09:00
|
|
|
const status = this.temperatureStatusFor(temp, actor);
|
|
|
|
|
if (currentStatus.direction === "cold" && temp <= current + 0.6 && !status.comfortable) continue;
|
|
|
|
|
if (currentStatus.direction === "hot" && temp >= current - 0.6 && !status.comfortable) continue;
|
|
|
|
|
const d = distXY(actor.x, actor.y, c.x, c.y);
|
|
|
|
|
const blockedPenalty = this.pathBlockedByFence?.(actor.x, actor.y, c.x, c.y, Math.max(14, radius * 0.72), { exclude: actor }) ? 480 : 0;
|
|
|
|
|
const score = status.discomfort * 110 + Math.abs(temp - targetTemp) * 2.8 + d * 0.22 + blockedPenalty + (c.extra || 0);
|
|
|
|
|
if (score < bestScore) {
|
|
|
|
|
bestScore = score;
|
2026-07-11 21:13:39 +09:00
|
|
|
const shelterLabel = c.targetItem?.type === "pipe" ? "\u571f\u7ba1" : (c.targetItem?.type === "nest_box" ? "\u5de3\u7bb1" : "\u5feb\u9069\u306a\u6e29\u5ea6\u5e2f");
|
2026-07-03 20:57:02 +09:00
|
|
|
best = { x: c.x, y: c.y, dead: false, detour: !c.targetItem, temperatureTarget: true, temperature: temp, label: shelterLabel };
|
|
|
|
|
if (c.targetItem) best.shelterItem = c.targetItem;
|
2026-07-01 14:41:05 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!best) return null;
|
|
|
|
|
const bestStatus = this.temperatureStatusFor(best.temperature, actor);
|
|
|
|
|
if (opts.requireComfort && !bestStatus.comfortable) return null;
|
|
|
|
|
if (!bestStatus.comfortable && bestStatus.discomfort >= currentStatus.discomfort - 0.65) return null;
|
|
|
|
|
best.temperatureStatus = bestStatus;
|
|
|
|
|
return best;
|
|
|
|
|
},
|
|
|
|
|
hasReachableComfortTemperatureSpot(actor) {
|
|
|
|
|
return !!this.findComfortTemperatureSpot?.(actor, { force: true, requireComfort: true });
|
|
|
|
|
},
|
|
|
|
|
sleepTargetTemperatureBlocked(actor, item) {
|
|
|
|
|
if (!actor || !item || item.dead) return false;
|
2026-07-03 00:45:22 +09:00
|
|
|
if (!(item.type === "grass_bed" || item.type === "bed" || item.type === "nest_box" || item.type === "pipe")) return false;
|
2026-07-01 14:41:05 +09:00
|
|
|
const current = this.feltTemperatureFor?.(actor) ?? this.temperatureAt?.(actor.x, actor.y, actor);
|
|
|
|
|
const currentStatus = this.temperatureStatusFor?.(current, actor);
|
|
|
|
|
if (!currentStatus || currentStatus.comfortable) return false;
|
2026-07-03 00:45:22 +09:00
|
|
|
const temp = item.type === "pipe" && this.pipeInteriorTemperatureFor
|
|
|
|
|
? this.pipeInteriorTemperatureFor(item, actor)
|
|
|
|
|
: this.temperatureAt?.(item.x, item.y, actor);
|
2026-07-01 14:41:05 +09:00
|
|
|
const status = this.temperatureStatusFor?.(temp, actor);
|
2026-07-05 18:01:36 +09:00
|
|
|
if (!status || status.comfortable) return false;
|
|
|
|
|
// A nest/pipe is still a valid night shelter when it improves a bad
|
|
|
|
|
// temperature, even if it cannot make the interior fully comfortable.
|
|
|
|
|
if (item.type === "nest_box" || item.type === "pipe") {
|
|
|
|
|
if (currentStatus.direction === "cold" && temp > current + 0.45) return false;
|
|
|
|
|
if (currentStatus.direction === "hot" && temp < current - 0.45) return false;
|
|
|
|
|
}
|
|
|
|
|
if (!this.hasReachableComfortTemperatureSpot?.(actor)) return false;
|
|
|
|
|
return true;
|
2026-07-01 14:41:05 +09:00
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
})(typeof window !== "undefined" ? window : globalThis);
|