325 lines
13 KiB
JavaScript
325 lines
13 KiB
JavaScript
|
|
"use strict";
|
||
|
|
|
||
|
|
(function (global) {
|
||
|
|
const World = global.World;
|
||
|
|
if (!World) throw new Error("World is not available for mixin: world_update.js");
|
||
|
|
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||
|
|
environmentScores() {
|
||
|
|
const alive = this.tarinai || [];
|
||
|
|
const avgStress = alive.length ? alive.reduce((sum, t) => sum + (t.stress || 0), 0) / alive.length : 0;
|
||
|
|
let fightPressure = 0;
|
||
|
|
let diseaseCount = 0;
|
||
|
|
for (const t of alive) {
|
||
|
|
if (!t || t.dead) continue;
|
||
|
|
if (t.state === "fight") fightPressure += 16;
|
||
|
|
if (t.state === "intimidate" || (t.intimidateTimer || 0) > 0.04 || (t.intimidatedTimer || 0) > 0.04) fightPressure += 7;
|
||
|
|
if ((t.hurtTimer || 0) > 0.04 || (t.lastBleedAt || -999) + 30 > (this.time || 0)) fightPressure += 5;
|
||
|
|
if (t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease) diseaseCount += 1;
|
||
|
|
}
|
||
|
|
let blood = 0;
|
||
|
|
for (const ef of this.effects || []) if (ef && !ef.dead && (ef.type === "bleed" || ef.type === "splat")) blood += 1;
|
||
|
|
const zunchi = this.itemCounts?.zunchi || 0;
|
||
|
|
const splat = this.itemCounts?.splat || 0;
|
||
|
|
const trace = this.itemCounts?.trace || 0;
|
||
|
|
const antCorpse = this.itemCounts?.ant_corpse || 0;
|
||
|
|
const security = clamp(100 - fightPressure - blood * 1.6, 0, 100);
|
||
|
|
const foodSpoilage = this.foodSpoilagePenalty || 0;
|
||
|
|
const hygiene = clamp(100 - zunchi * 2.0 - splat * 1.2 - trace * 0.30 - antCorpse * 0.65 - blood * 1.8 - diseaseCount * 8.0 - foodSpoilage, 0, 100);
|
||
|
|
const happiness = clamp(100 - Math.max(0, 100 - security) * 0.32 - Math.max(0, 100 - hygiene) * 0.24 - avgStress * 0.42, 0, 100);
|
||
|
|
return { security, hygiene, happiness };
|
||
|
|
},
|
||
|
|
|
||
|
|
|
||
|
|
registerFoodSpoilage(amount = 0, item = null) {
|
||
|
|
const value = Math.max(0, Number(amount) || 0);
|
||
|
|
if (!value) return;
|
||
|
|
this.foodSpoilagePenalty = clamp((this.foodSpoilagePenalty || 0) + value, 0, 35);
|
||
|
|
this.foodSpoilageEvents = (this.foodSpoilageEvents || 0) + 1;
|
||
|
|
},
|
||
|
|
|
||
|
|
observeHighlightKind(t) {
|
||
|
|
const s = this.selected;
|
||
|
|
if (!s || !t || t === s || t.dead || this.tool !== "observe") return "";
|
||
|
|
const tFamilyKey = this.tarinaiFamilyKey(t);
|
||
|
|
if ((s.parents || []).includes(tFamilyKey)) return "parent";
|
||
|
|
if ((s.children || []).includes(tFamilyKey)) return "child";
|
||
|
|
const rel = s.relationTo ? s.relationTo(t.id) : null;
|
||
|
|
if (rel && (rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD) return "friend";
|
||
|
|
if (rel && (rel.fear || 0) >= 8) return "enemy";
|
||
|
|
return "";
|
||
|
|
},
|
||
|
|
|
||
|
|
nestBoxOccupants(box, limit = 5) {
|
||
|
|
if (!box || box.dead || box.type !== "nest_box") return [];
|
||
|
|
const occupants = this.tarinai.filter(t => t && !t.dead && t.insideNestBoxId === box.id);
|
||
|
|
return Number.isFinite(limit) ? occupants.slice(0, Math.max(0, limit)) : occupants;
|
||
|
|
},
|
||
|
|
|
||
|
|
pointerNestBoxInfo() {
|
||
|
|
const p = this.pointer;
|
||
|
|
if (!p?.inside) return null;
|
||
|
|
let best = null, bestD = Infinity;
|
||
|
|
for (const it of this.nearbyItems(p.x, p.y, 140)) {
|
||
|
|
if (!it || it.dead || it.type !== "nest_box") continue;
|
||
|
|
const d = distXY(p.x, p.y, it.x, it.y);
|
||
|
|
if (d < bestD && d <= Math.max(80, it.r * 2.1)) { best = it; bestD = d; }
|
||
|
|
}
|
||
|
|
if (!best) return null;
|
||
|
|
const occupants = this.nestBoxOccupants(best, Infinity);
|
||
|
|
const capacity = this.nestBoxCapacity(best);
|
||
|
|
return { box: best, occupants, capacity };
|
||
|
|
},
|
||
|
|
|
||
|
|
nestBoxToolTipCount() {
|
||
|
|
const pointed = this.pointerNestBoxInfo?.();
|
||
|
|
if (pointed?.box) return { current: pointed.occupants.length, max: pointed.capacity || this.nestBoxCapacity(pointed.box) };
|
||
|
|
const boxes = (this.items || []).filter(it => it && !it.dead && it.type === "nest_box");
|
||
|
|
if (!boxes.length) return { current: 0, max: this.nestBoxCapacity() };
|
||
|
|
let current = 0, max = 0;
|
||
|
|
for (const box of boxes) {
|
||
|
|
current += this.nestBoxOccupants(box, Infinity).length;
|
||
|
|
max += this.nestBoxCapacity(box);
|
||
|
|
}
|
||
|
|
return { current, max };
|
||
|
|
},
|
||
|
|
|
||
|
|
blastZunchiFrom(x, y, radius, strength = 1) {
|
||
|
|
let moved = 0;
|
||
|
|
for (const it of this.nearbyItems(x, y, radius + 60)) {
|
||
|
|
if (!it || it.dead || it.type !== "zunchi") continue;
|
||
|
|
let dx = it.x - x;
|
||
|
|
let dy = it.y - y;
|
||
|
|
let d = Math.hypot(dx, dy) || 1;
|
||
|
|
if (d > radius) continue;
|
||
|
|
if (d < 0.001) {
|
||
|
|
const a = rand(0, Math.PI * 2);
|
||
|
|
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
||
|
|
}
|
||
|
|
const p = clamp(1 - d / radius, 0, 1);
|
||
|
|
it.vx = (it.vx || 0) + dx / d * (130 + p * 430) * strength + rand(-45, 45);
|
||
|
|
it.vy = (it.vy || 0) + dy / d * (130 + p * 430) * strength + rand(-45, 45);
|
||
|
|
it.amount = Math.max(10, (it.amount || 80) - p * 8);
|
||
|
|
it.stage = "fresh";
|
||
|
|
moved += 1;
|
||
|
|
if (moved < 12) this.effects.push(new Effect("zunchi_miasma", it.x, it.y - 4, { vx: (it.vx || 0) * 0.15, vy: (it.vy || 0) * 0.15 - 18, size: rand(9, 18), life: rand(0.36, 0.62), color: "rgba(58,102,38,0.48)" }));
|
||
|
|
}
|
||
|
|
if (moved) this.drawListDirty = true;
|
||
|
|
return moved;
|
||
|
|
},
|
||
|
|
|
||
|
|
update(dt) {
|
||
|
|
if (this.paused) return;
|
||
|
|
dt *= this.speed;
|
||
|
|
dt = Math.min(dt, 0.12 * this.speed);
|
||
|
|
|
||
|
|
this.time += dt;
|
||
|
|
const previousDay = this.day || 1;
|
||
|
|
this.day = Math.floor(this.time / CONFIG.dayLength) + 1;
|
||
|
|
if (this.day !== previousDay) {
|
||
|
|
for (const t of this.tarinai || []) if (t) t.personalityDaily = { day: this.day, total: 0, byKey: {} };
|
||
|
|
}
|
||
|
|
if (this.pointer) this.pointer.motion = Math.max(0, (this.pointer.motion || 0) - dt * 2.4);
|
||
|
|
this.shakeTimer = Math.max(0, (this.shakeTimer || 0) - dt);
|
||
|
|
if ((this.shakeTimer || 0) <= 0) { this.shakeDuration = 0; this.shakeStrength = 0; }
|
||
|
|
this.updateWeather(dt);
|
||
|
|
this.foodSpoilagePenalty = Math.max(0, (this.foodSpoilagePenalty || 0) - dt * 0.010);
|
||
|
|
this.updateTerrainDirtyState?.(dt);
|
||
|
|
this.rebuildSpatial();
|
||
|
|
|
||
|
|
const perf = this.performanceLevel();
|
||
|
|
const itemCountBefore = this.items.length;
|
||
|
|
for (const it of this.items) it.update(dt, this);
|
||
|
|
if (this.itemCounts?.ball) {
|
||
|
|
this.rebuildSpatial();
|
||
|
|
this.resolveBallBallCollisions(dt);
|
||
|
|
this.rebuildSpatial();
|
||
|
|
}
|
||
|
|
this.updateAnts?.(dt);
|
||
|
|
|
||
|
|
for (const ef of this.effects) ef.update(dt);
|
||
|
|
|
||
|
|
for (const t of this.tarinai) {
|
||
|
|
t.update(dt);
|
||
|
|
}
|
||
|
|
this.rebuildSpatial();
|
||
|
|
this.limitBallChasers();
|
||
|
|
this.resolveBallInteractions(dt);
|
||
|
|
this.resolveTarinaiHighSpeedCollisions(dt);
|
||
|
|
|
||
|
|
this.compactTimer += dt;
|
||
|
|
const compactInterval = perf >= 2 ? 0.95 : 0.58;
|
||
|
|
let itemsCompacted = false;
|
||
|
|
if (this.compactTimer >= compactInterval) {
|
||
|
|
itemsCompacted = this.compactItems();
|
||
|
|
this.compactEffects();
|
||
|
|
this.compactTarinai();
|
||
|
|
this.compactAnts?.();
|
||
|
|
this.compactTimer = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.countsTimer += dt;
|
||
|
|
const countsInterval = perf >= 2 ? 0.85 : 0.42;
|
||
|
|
if (this.countsTimer >= countsInterval || itemsCompacted || this.items.length !== itemCountBefore) {
|
||
|
|
this.updateItemCounts();
|
||
|
|
this.updateEffectCounts();
|
||
|
|
this.countsTimer = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ((this.outOfBoundsCheckAt || -999) + 1.0 <= this.time) {
|
||
|
|
this.outOfBoundsCheckAt = this.time;
|
||
|
|
this.sanitizeOutOfBounds();
|
||
|
|
}
|
||
|
|
|
||
|
|
this.drawSortTimer += dt;
|
||
|
|
const sortInterval = perf >= 2 ? 0.16 : perf === 1 ? 0.11 : 0.07;
|
||
|
|
if (this.drawSortTimer >= sortInterval) {
|
||
|
|
this.drawListDirty = true;
|
||
|
|
this.drawSortTimer = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
const phase = this.phaseName();
|
||
|
|
if (phase !== this.lastPhase) {
|
||
|
|
this.lastPhase = phase;
|
||
|
|
this.emit("world:phase", { phase });
|
||
|
|
audio.phase();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.tarinai.length > 0 && this.tarinai.length <= 10 && this.time - (this.lastBirthAt || -999) > 12 && Math.random() < dt * 0.022) {
|
||
|
|
const spot = this.edgeSpawnPoint(null, null, 42, true);
|
||
|
|
const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, type: baseSpriteId(), entryTimer: 2.0 });
|
||
|
|
t.wanderAngle = spot.angle;
|
||
|
|
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.35);
|
||
|
|
audio.birth?.();
|
||
|
|
this.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
const grassCount = this.itemCounts.grass || 0;
|
||
|
|
if (grassCount < Math.min(24, this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 96)) && Math.random() < dt * 0.036) {
|
||
|
|
const spot = this.findGrassPlantingSpot(rand(60, this.w - 60), rand(60, this.h - 60), {
|
||
|
|
allowOriginal: true,
|
||
|
|
minRadius: 28,
|
||
|
|
maxRadius: 140,
|
||
|
|
attempts: 24,
|
||
|
|
});
|
||
|
|
if (spot) {
|
||
|
|
const sprout = new Item("grass", spot.x, spot.y);
|
||
|
|
sprout.amount = rand(22, 48);
|
||
|
|
sprout.growth = rand(0.12, 0.30);
|
||
|
|
this.items.push(sprout);
|
||
|
|
this.itemCounts.grass = grassCount + 1;
|
||
|
|
this.markTerrainDirty?.("grass-spawn");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.weather === "light_rain" && (this.itemCounts.water || 0) < 24 && Math.random() < dt * 0.12) {
|
||
|
|
const drop = new Item("water", rand(58, this.w - 58), rand(58, this.h - 58));
|
||
|
|
drop.amount = rand(24, 58);
|
||
|
|
this.items.push(drop);
|
||
|
|
this.itemCounts.water = (this.itemCounts.water || 0) + 1;
|
||
|
|
this.markTerrainDirty?.("rain-water");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Math.random() < dt * 0.004) {
|
||
|
|
const entries = [
|
||
|
|
"\u4f55\u5339\u304b\u304c\u3001\u5168\u54e1\u3067\u306f\u306a\u3044\u304c\u3001\u540c\u3058\u65b9\u5411\u3092\u898b\u305f\u3002",
|
||
|
|
"\u98df\u3079\u7269\u306f\u3042\u308b\u3002\u8db3\u308a\u3066\u3044\u308b\u304b\u306f\u5225\u554f\u984c\u3002",
|
||
|
|
"\u4e00\u5339\u304c\u7720\u308a\u3001\u8fd1\u304f\u306e\u5225\u306e\u4e00\u5339\u3082\u7720\u308b\u3053\u3068\u3092\u8003\u3048\u305f\u3002",
|
||
|
|
"\u8349\u306e\u8fd1\u304f\u306b\u3001\u77ed\u304f\u3066\u610f\u5473\u306e\u306a\u3044\u5217\u304c\u3067\u304d\u305f\u3002",
|
||
|
|
`${this.phaseName()}\u3002\u6c17\u5206\u306f\u3086\u3063\u304f\u308a\u5909\u308f\u3063\u3066\u3044\u308b\u3002`
|
||
|
|
];
|
||
|
|
this.log(pick(entries));
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
sanitizeOutOfBounds() {
|
||
|
|
const pad = Math.max(28, CONFIG.worldPadding || 30);
|
||
|
|
const far = Math.max(360, Math.max(this.w || 1000, this.h || 720) * 0.75);
|
||
|
|
let changedItems = false;
|
||
|
|
const repairPoint = (obj, radius = 12) => {
|
||
|
|
if (!obj) return "delete";
|
||
|
|
if (!Number.isFinite(obj.x) || !Number.isFinite(obj.y)) return "delete";
|
||
|
|
const minX = pad - radius;
|
||
|
|
const maxX = (this.w || 1000) - pad + radius;
|
||
|
|
const minY = pad - radius;
|
||
|
|
const maxY = (this.h || 720) - pad + radius;
|
||
|
|
if (obj.x >= minX && obj.x <= maxX && obj.y >= minY && obj.y <= maxY) return "ok";
|
||
|
|
if (obj.x < -far || obj.x > (this.w || 1000) + far || obj.y < -far || obj.y > (this.h || 720) + far) return "delete";
|
||
|
|
obj.x = clamp(obj.x, pad, (this.w || 1000) - pad);
|
||
|
|
obj.y = clamp(obj.y, pad, (this.h || 720) - pad);
|
||
|
|
if (Number.isFinite(obj.vx)) obj.vx *= -0.18;
|
||
|
|
if (Number.isFinite(obj.vy)) obj.vy *= -0.18;
|
||
|
|
return "moved";
|
||
|
|
};
|
||
|
|
for (const it of this.items || []) {
|
||
|
|
const result = repairPoint(it, it.r || 12);
|
||
|
|
if (result === "delete") { it.amount = 0; changedItems = true; }
|
||
|
|
else if (result === "moved") changedItems = true;
|
||
|
|
}
|
||
|
|
for (const ef of this.effects || []) {
|
||
|
|
const result = repairPoint(ef, ef.size || 12);
|
||
|
|
if (result === "delete") ef.life = 0;
|
||
|
|
}
|
||
|
|
for (const t of this.tarinai || []) {
|
||
|
|
if (!t || t.dead) continue;
|
||
|
|
const result = repairPoint(t, t.radius || 22);
|
||
|
|
if (result === "delete") {
|
||
|
|
t.x = clamp(Number.isFinite(t.x) ? t.x : this.w * 0.5, pad, this.w - pad);
|
||
|
|
t.y = clamp(Number.isFinite(t.y) ? t.y : this.h * 0.5, pad, this.h - pad);
|
||
|
|
t.vx = 0;
|
||
|
|
t.vy = 0;
|
||
|
|
t.entryTimer = 0;
|
||
|
|
t.die(HEALTH?.CAUSES?.outOfBounds || "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664");
|
||
|
|
} else if (result === "moved") {
|
||
|
|
t.x = clamp(Number.isFinite(t.x) ? t.x : this.w * 0.5, pad, this.w - pad);
|
||
|
|
t.y = clamp(Number.isFinite(t.y) ? t.y : this.h * 0.5, pad, this.h - pad);
|
||
|
|
t.vx = 0;
|
||
|
|
t.vy = 0;
|
||
|
|
t.entryTimer = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let changedAnts = false;
|
||
|
|
for (const ant of this.ants || []) {
|
||
|
|
if (!ant || ant.dead) continue;
|
||
|
|
const result = repairPoint(ant, ant.kind === "queen" ? 18 : 10);
|
||
|
|
if (result === "delete") {
|
||
|
|
ant.dead = true;
|
||
|
|
changedAnts = true;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (result === "moved") {
|
||
|
|
ant.vx = 0;
|
||
|
|
ant.vy = 0;
|
||
|
|
if (ant.state === "drag") {
|
||
|
|
ant.state = "return";
|
||
|
|
ant.targetId = "";
|
||
|
|
ant.targetToken = 0;
|
||
|
|
ant.targetRef = null;
|
||
|
|
}
|
||
|
|
changedAnts = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (changedAnts) this.compactAnts?.();
|
||
|
|
if (changedItems) {
|
||
|
|
this.compactItems();
|
||
|
|
this.updateItemCounts();
|
||
|
|
this.rebuildSpatial();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
updateWeather(dt) {
|
||
|
|
this.nextWeatherChange -= dt;
|
||
|
|
if (this.nextWeatherChange > 0) return;
|
||
|
|
const current = this.weather || "sunny";
|
||
|
|
const choices = current === "sunny"
|
||
|
|
? ["sunny", "cloudy", "light_rain"]
|
||
|
|
: current === "cloudy"
|
||
|
|
? ["sunny", "cloudy", "light_rain"]
|
||
|
|
: ["sunny", "cloudy"];
|
||
|
|
const next = pick(choices);
|
||
|
|
this.weather = next;
|
||
|
|
this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
|
||
|
|
if (next !== current) this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`);
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
})(typeof window !== "undefined" ? window : globalThis);
|