"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; }, seasonTemperatureBias() { return this.seasonIndex?.() === 1 ? (CONFIG.summerTemperatureBoost ?? 5) : 0; }, 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; } temp += this.seasonTemperatureBias?.() ?? 0; 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; }, 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; }, 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); const fanRange = 320; for (const it of this.nearbyItems(x, y, Math.max(range + 8, fanRange + 40))) { if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove" && it.type !== "fan")) continue; if (global.TarinaiSignalSystem?.powerOverride?.(it) === false) continue; if (it.type === "fan") { const wind = global.TarinaiItemDynamicBallSystem?.fanWindAt?.(it, x, y, { range: fanRange }) || null; if (!wind) continue; temp -= 5 * wind.falloff; continue; } 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; } const parasolSystem = global.TarinaiParasolSystem || global.TarinaiRainShelterSystem; const weatherBlocked = Boolean(actor && parasolSystem?.blocksWeatherAt?.(this, x, y, actor)); temp += weatherBlocked ? 0 : (this.weatherFeltTemperatureOffsetAt?.(x, y, actor) || 0); if (actor && !weatherBlocked && (actor.state === "sunbath" || (actor.sunbathTimer || 0) > 0.04)) temp += 3; return this.temperatureNumber(temp); }, 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; return this.temperatureNumber(temp + clamp(toward, -5, 5)); }, feltTemperatureFor(actor, x = actor?.x, y = actor?.y) { if (!actor) return this.temperatureAt(x, y); 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; } } 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")), }; }, 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"); const weather = String(this.weather || "sunny"); if ((direction === "hot" && weather === "sunny") || (direction === "cold" && weather === "light_rain")) types.push("rain_shelter"); 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; }, 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 = []; const pushCandidate = (x, y, extra = 0, targetItem = null, tempOverride = null) => candidates.push({ x: clamp(x, radius + 8, (this.w || 1000) - radius - 8), y: clamp(y, radius + 8, (this.h || 720) - radius - 8), extra, targetItem, tempOverride, }); 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); for (const it of this.temperatureComfortCandidateItems?.(currentStatus.direction) || this.items || []) { if (!it || it.dead) continue; 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; } 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; if (currentStatus.direction === "cold" && it.type !== "stove") continue; 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; } 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; const temp = c.tempOverride != null && Number.isFinite(Number(c.tempOverride)) ? Number(c.tempOverride) : this.temperatureAt(c.x, c.y, actor); 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; const shelterLabel = c.targetItem?.type === "pipe" ? "\u571f\u7ba1" : (c.targetItem?.type === "nest_box" ? "\u5de3\u7bb1" : "\u5feb\u9069\u306a\u6e29\u5ea6\u5e2f"); 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; } } 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; if (!(item.type === "grass_bed" || item.type === "bed" || item.type === "nest_box" || item.type === "pipe")) return false; const current = this.feltTemperatureFor?.(actor) ?? this.temperatureAt?.(actor.x, actor.y, actor); const currentStatus = this.temperatureStatusFor?.(current, actor); if (!currentStatus || currentStatus.comfortable) return false; const temp = item.type === "pipe" && this.pipeInteriorTemperatureFor ? this.pipeInteriorTemperatureFor(item, actor) : this.temperatureAt?.(item.x, item.y, actor); const status = this.temperatureStatusFor?.(temp, actor); 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; }, })); })(typeof window !== "undefined" ? window : globalThis);