tarinai/js/world_temperature_system.js

238 lines
12 KiB
JavaScript
Raw Normal View History

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;
},
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;
}
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;
},
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);
for (const it of this.nearbyItems(x, y, range + 8)) {
if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove")) 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;
}
if (actor && (actor.state === "sunbath" || (actor.sunbathTimer || 0) > 0.04)) temp += 3;
return this.temperatureNumber(temp);
},
feltTemperatureFor(actor, x = actor?.x, y = actor?.y) {
if (!actor) return this.temperatureAt(x, y);
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")),
};
},
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) => candidates.push({
x: clamp(x, radius + 8, (this.w || 1000) - radius - 8),
y: clamp(y, radius + 8, (this.h || 720) - radius - 8),
extra,
});
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.items || []) {
if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove")) continue;
if (currentStatus.direction === "cold" && it.type !== "stove") continue;
if (currentStatus.direction === "hot" && it.type !== "dry_ice") 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 = 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;
best = { x: c.x, y: c.y, dead: false, detour: true, temperatureTarget: true, temperature: temp, label: "\u5FEB\u9069\u306A\u6E29\u5EA6\u5E2F" };
}
}
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")) 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;
if (!this.hasReachableComfortTemperatureSpot?.(actor)) return false;
const temp = this.temperatureAt?.(item.x, item.y, actor);
const status = this.temperatureStatusFor?.(temp, actor);
return Boolean(status && !status.comfortable);
},
}));
})(typeof window !== "undefined" ? window : globalThis);