hm
This commit is contained in:
parent
7455d630fc
commit
4d3fc2cd47
84 changed files with 3429 additions and 3480 deletions
238
js/world_temperature_system.js
Normal file
238
js/world_temperature_system.js
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
"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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue