2026-06-21 14:09:35 +09:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
(function (global) {
|
|
|
|
|
const World = global.World;
|
|
|
|
|
if (!World) throw new Error("World is not available for mixin: world_update.js");
|
2026-06-24 21:20:53 +09:00
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
2026-06-24 21:20:53 +09:00
|
|
|
environmentScores(force = false) {
|
|
|
|
|
const nowScore = this.time || 0;
|
|
|
|
|
if (!force && this.environmentScoreCache && nowScore < (this.nextEnvironmentScoreAt || 0)) return this.environmentScoreCache;
|
2026-06-21 14:09:35 +09:00
|
|
|
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);
|
2026-06-24 21:20:53 +09:00
|
|
|
this.environmentScoreCache = { security, hygiene, happiness };
|
|
|
|
|
this.nextEnvironmentScoreAt = nowScore + 3.0;
|
|
|
|
|
return this.environmentScoreCache;
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-22 02:03:19 +09:00
|
|
|
hourOfDay(timeValue = this.time || 0) {
|
|
|
|
|
return ((((timeValue || 0) / Math.max(1, CONFIG.dayLength || 120)) * 24) % 24 + 24) % 24;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
colonyMoodDefinition(id = "relaxed") {
|
|
|
|
|
const defs = {
|
2026-06-28 13:40:41 +09:00
|
|
|
calm: { id: "calm", label: "\u7a4f\u3084\u304b", description: "\u793e\u4ea4\u6027\u304c\u4e0a\u304c\u308b", effects: { personality: { sociability: 0.42 } } },
|
|
|
|
|
prickly: { id: "prickly", label: "\u7dca\u5f35", description: "\u653b\u6483\u6027\u304c\u4e0a\u304c\u308b", effects: { personality: { aggression: 0.46 } } },
|
|
|
|
|
stinky: { id: "stinky", label: "\u4e0d\u885b\u751f", description: "\u958b\u653e\u6027\u304c\u4e0b\u304c\u308b", effects: { personality: { openness: -0.44 } } },
|
|
|
|
|
weaklings: { id: "weaklings", label: "\u5f31\u4f53\u5316", description: "\u305a\u3093\u3061\u3069\u308c\u3044\u4ee5\u5916\u306e\u30b9\u30c8\u30ec\u30b9\u304c\u4e0b\u304c\u308b", effects: { personality: {} } },
|
|
|
|
|
zunda_party: { id: "zunda_party", label: "\u305a\u3093\u3060\u96c6\u4e2d", description: "\u793e\u4ea4\u6027\u3068\u958b\u653e\u6027\u304c\u4e0a\u304c\u308a\u3001\u30b9\u30c8\u30ec\u30b9\u304c\u4e0b\u304c\u308b", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
|
|
|
|
ball_party: { id: "ball_party", label: "\u30dc\u30fc\u30eb\u96c6\u4e2d", description: "\u793e\u4ea4\u6027\u3068\u958b\u653e\u6027\u304c\u4e0a\u304c\u308a\u3001\u30b9\u30c8\u30ec\u30b9\u304c\u4e0b\u304c\u308b", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
|
|
|
|
sun_party: { id: "sun_party", label: "\u65e5\u5149\u6d74\u50be\u5411", description: "\u65e5\u5149\u6d74\u306e\u983b\u5ea6\u304c\u4e0a\u304c\u308b", effects: { personality: {} } },
|
|
|
|
|
ant_overrun: { id: "ant_overrun", label: "\u30a2\u30ea\u5897\u52a0", description: "\u30a2\u30ea\u3092\u898b\u308b\u3068\u30d1\u30cb\u30c3\u30af\u306b\u306a\u308a\u3084\u3059\u3044", effects: { personality: {} } },
|
|
|
|
|
devastation: { id: "devastation", label: "\u58ca\u6ec5\u5bf8\u524d", description: "\u30b3\u30ed\u30cb\u30fc\u306e\u5b58\u7d9a\u304c\u304b\u306a\u308a\u5371\u3046\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
crisis: { id: "crisis", label: "\u5371\u6a5f", description: "\u3051\u304c\u3084\u6d88\u8017\u304c\u76ee\u7acb\u3061\u3001\u7acb\u3066\u76f4\u3057\u304c\u5fc5\u8981\u306a\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
confusion: { id: "confusion", label: "\u6df7\u4e71", description: "\u9a5a\u304d\u3084\u30d1\u30cb\u30c3\u30af\u3067\u843d\u3061\u7740\u304d\u304c\u5931\u308f\u308c\u3066\u3044\u307e\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
fearful: { id: "fearful", label: "\u6050\u6016", description: "\u5b89\u5168\u3092\u6c42\u3081\u308b\u500b\u4f53\u304c\u591a\u304f\u3001\u5371\u967a\u3092\u907f\u3051\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
weakened: { id: "weakened", label: "\u8870\u5f31", description: "\u4f53\u529b\u3084\u5143\u6c17\u304c\u843d\u3061\u3001\u52d5\u304d\u304c\u5f31\u304f\u306a\u3063\u3066\u3044\u307e\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
disease_spread: { id: "disease_spread", label: "\u75c5\u6c17\u8513\u5ef6", description: "\u75c5\u6c17\u306e\u500b\u4f53\u304c\u76ee\u7acb\u3061\u3001\u8abf\u5b50\u3092\u5d29\u3057\u3084\u3059\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
overcrowded: { id: "overcrowded", label: "\u904e\u5bc6", description: "\u500b\u4f53\u540c\u58eb\u306e\u8ddd\u96e2\u304c\u8fd1\u304f\u3001\u3076\u3064\u304b\u308a\u3084\u3059\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
isolated: { id: "isolated", label: "\u5b64\u7acb", description: "\u4ef2\u9593\u304c\u5c11\u306a\u3044\u3001\u307e\u305f\u306f\u8ddd\u96e2\u304c\u96e2\u308c\u3066\u5bc2\u3057\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
breeding: { id: "breeding", label: "\u7e41\u6b96", description: "\u7e41\u6b96\u3084\u89aa\u5bc6\u306a\u884c\u52d5\u304c\u8d77\u3053\u308a\u3084\u3059\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
happy: { id: "happy", label: "\u5e78\u798f", description: "\u30b9\u30c8\u30ec\u30b9\u304c\u5c11\u306a\u304f\u3001\u4f59\u88d5\u306e\u3042\u308b\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
|
|
|
|
relaxed: { id: "relaxed", label: "\u5b89\u5b9a", description: "\u5927\u304d\u306a\u554f\u984c\u304c\u5c11\u306a\u304f\u3001\u843d\u3061\u7740\u3044\u305f\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
2026-06-22 02:03:19 +09:00
|
|
|
};
|
|
|
|
|
return defs[id] || defs.relaxed;
|
|
|
|
|
},
|
2026-06-28 13:40:41 +09:00
|
|
|
|
|
|
|
|
evaluateColonyMood(force = false) {
|
|
|
|
|
const now = this.time || 0;
|
|
|
|
|
if (!force && this.colonyMood && now < (this.nextColonyMoodEvalAt || 0)) return this.colonyMood;
|
|
|
|
|
const alive = (this.tarinai || []).filter(t => t && !t.dead);
|
|
|
|
|
const n = alive.length;
|
|
|
|
|
const avg = (fn) => n ? alive.reduce((sum, t) => sum + (Number(fn(t)) || 0), 0) / n : 0;
|
|
|
|
|
const count = (fn) => alive.reduce((sum, t) => sum + (fn(t) ? 1 : 0), 0);
|
|
|
|
|
const ratio = (value) => n ? value / n : 0;
|
|
|
|
|
const avgEnergy = avg(t => t.energy || 0);
|
|
|
|
|
const avgStress = avg(t => t.stress || 0);
|
|
|
|
|
const avgHunger = avg(t => t.hunger || 0);
|
|
|
|
|
const avgSafety = avg(t => t.needs?.safety || 0);
|
|
|
|
|
const panicCount = count(t => t.state === "panic" || (t.fearTimer || 0) > 0.2 || (t.panicTargetUntil || 0) > now);
|
|
|
|
|
const safetyHighCount = count(t => (t.needs?.safety || 0) >= 52 || (t.fearTimer || 0) > 0.35);
|
|
|
|
|
const weakCount = count(t => (t.energy || 0) < 45 || (t.hunger || 0) > 88 || (t.needs?.health || 0) > 58);
|
|
|
|
|
const diseasedCount = count(t => t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease);
|
|
|
|
|
const breedingCount = count(t => (t.loveMochiTimer || 0) > 0.1 || t.state === "birth_ritual" || (t.birthRitualTimer || 0) > 0.1);
|
|
|
|
|
const recentDeaths = (Array.isArray(this.recentDeathTimes) ? this.recentDeathTimes : []).filter(t => now - (Number(t) || -999) <= 30).length;
|
|
|
|
|
let nearestAvg = Infinity;
|
|
|
|
|
if (n >= 2) {
|
|
|
|
|
let totalNearest = 0;
|
|
|
|
|
for (const a of alive) {
|
|
|
|
|
let best = Infinity;
|
|
|
|
|
for (const b of alive) {
|
|
|
|
|
if (a === b) continue;
|
|
|
|
|
const dx = (a.x || 0) - (b.x || 0);
|
|
|
|
|
const dy = (a.y || 0) - (b.y || 0);
|
|
|
|
|
const d2 = dx * dx + dy * dy;
|
|
|
|
|
if (d2 < best) best = d2;
|
|
|
|
|
}
|
|
|
|
|
if (Number.isFinite(best)) totalNearest += Math.sqrt(best);
|
|
|
|
|
}
|
|
|
|
|
nearestAvg = totalNearest / n;
|
|
|
|
|
}
|
|
|
|
|
const recentShock = (now - (this.lastShootAt || -999) <= 8) || (now - (this.lastGenkotsuAt || -999) <= 8);
|
|
|
|
|
const panicRatio = ratio(panicCount);
|
|
|
|
|
const safetyRatio = ratio(safetyHighCount);
|
|
|
|
|
const weakRatio = ratio(weakCount);
|
|
|
|
|
const diseaseRatio = ratio(diseasedCount);
|
|
|
|
|
const breedingRatio = ratio(breedingCount);
|
|
|
|
|
const fieldId = String(this.fieldType || "garden");
|
|
|
|
|
const overcrowdLimit = fieldId === "cage" ? 20 : (fieldId === "park" ? 100 : 50);
|
|
|
|
|
const denseCrowd = Number.isFinite(nearestAvg) && nearestAvg < 84 && n >= Math.max(8, Math.floor(overcrowdLimit * 0.55));
|
|
|
|
|
const candidates = [];
|
|
|
|
|
const add = (id, priority, ok) => { if (ok) candidates.push({ id, priority }); };
|
|
|
|
|
|
|
|
|
|
// Severe or multi-signal situations are evaluated first. Simple population
|
|
|
|
|
// and comfort labels are deliberately fallback states so they do not mask
|
|
|
|
|
// richer current events.
|
|
|
|
|
add("devastation", 120, n <= 0 || recentDeaths >= 3 || (n <= 2 && this.deadCount > 0 && (recentDeaths > 0 || weakRatio >= 0.35 || panicRatio >= 0.20)));
|
|
|
|
|
add("crisis", 110, recentDeaths >= 2 || (avgEnergy < 38 && weakRatio >= 0.34) || (avgHunger > 88 && weakRatio >= 0.40));
|
|
|
|
|
add("confusion", 100, (recentShock && (panicRatio >= 0.10 || safetyRatio >= 0.18)) || panicRatio >= 0.25);
|
|
|
|
|
add("fearful", 90, (avgSafety >= 50 && safetyRatio >= 0.28) || (recentShock && safetyRatio >= 0.18));
|
|
|
|
|
add("disease_spread", 82, diseaseRatio >= 0.25 || (diseasedCount >= 3 && diseaseRatio >= 0.16));
|
|
|
|
|
add("weakened", 74, (avgEnergy < 54 && weakRatio >= 0.18) || avgHunger > 82 || weakRatio >= 0.34);
|
|
|
|
|
add("breeding", 66, breedingRatio >= 0.10 || breedingCount >= 2 || now - (this.lastBirthAt || -999) <= 45);
|
|
|
|
|
add("happy", 46, avgStress < 28 && panicRatio < 0.08 && diseaseRatio < 0.12 && weakRatio < 0.18);
|
|
|
|
|
add("overcrowded", 34, n >= overcrowdLimit || denseCrowd);
|
|
|
|
|
add("isolated", 28, n <= 1 || (n <= 3 && nearestAvg > Math.max(180, Math.min(this.w || 900, this.h || 600) * 0.32)));
|
|
|
|
|
|
|
|
|
|
let id = "relaxed";
|
|
|
|
|
if (candidates.length) {
|
|
|
|
|
candidates.sort((a, b) => b.priority - a.priority);
|
|
|
|
|
id = candidates[0].id;
|
|
|
|
|
}
|
|
|
|
|
const next = this.colonyMoodDefinition(id);
|
|
|
|
|
this.colonyMood = next;
|
|
|
|
|
this.nextColonyMoodEvalAt = now + 1.25;
|
|
|
|
|
return next;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
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";
|
2026-06-24 13:48:22 +09:00
|
|
|
const friendThreshold = typeof FRIEND_AFFINITY_THRESHOLD === "number" ? FRIEND_AFFINITY_THRESHOLD : 14;
|
2026-06-21 14:09:35 +09:00
|
|
|
const rel = s.relationTo ? s.relationTo(t.id) : null;
|
2026-06-24 13:48:22 +09:00
|
|
|
const reverse = t.relationTo ? t.relationTo(s.id) : null;
|
|
|
|
|
if ((rel && (rel.affinity || 0) >= friendThreshold) || (reverse && (reverse.affinity || 0) >= friendThreshold)) return "friend";
|
|
|
|
|
if ((rel && (rel.fear || 0) >= 8) || (reverse && (reverse.fear || 0) >= 8)) return "enemy";
|
2026-06-21 14:09:35 +09:00
|
|
|
return "";
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-24 13:48:22 +09:00
|
|
|
resolveOwnedBedSleepConflicts() {
|
|
|
|
|
const alive = (this.tarinai || []).filter(t => t && !t.dead);
|
|
|
|
|
if (alive.length < 2) return 0;
|
|
|
|
|
const friendThreshold = typeof FRIEND_AFFINITY_THRESHOLD === "number" ? FRIEND_AFFINITY_THRESHOLD : 14;
|
|
|
|
|
const isFriendPair = (a, b) => {
|
|
|
|
|
const relAB = a?.relationTo ? a.relationTo(b?.id) : null;
|
|
|
|
|
const relBA = b?.relationTo ? b.relationTo(a?.id) : null;
|
|
|
|
|
return Boolean((relAB && (relAB.affinity || 0) >= friendThreshold) || (relBA && (relBA.affinity || 0) >= friendThreshold));
|
|
|
|
|
};
|
|
|
|
|
const bedSleepers = new Map();
|
|
|
|
|
for (const sleeper of alive) {
|
|
|
|
|
if (!(sleeper.sleeping || sleeper.state === "sleep")) continue;
|
|
|
|
|
const bed = sleeper.target;
|
|
|
|
|
if (!bed || bed.dead || bed.type !== "grass_bed") continue;
|
|
|
|
|
if (distXY(sleeper.x, sleeper.y, bed.x, bed.y) > Math.max((bed.r || 18) * 1.55, 32)) continue;
|
|
|
|
|
if (!bedSleepers.has(bed.id)) bedSleepers.set(bed.id, []);
|
|
|
|
|
bedSleepers.get(bed.id).push(sleeper);
|
|
|
|
|
}
|
|
|
|
|
let conflicts = 0;
|
|
|
|
|
for (const intruder of alive) {
|
|
|
|
|
if (!(intruder.state === "seek_bed" || intruder.state === "sleep")) continue;
|
|
|
|
|
const bed = intruder.target;
|
|
|
|
|
if (!bed || bed.dead || bed.type !== "grass_bed") continue;
|
|
|
|
|
const sleepers = bedSleepers.get(bed.id) || [];
|
|
|
|
|
if (!sleepers.length) continue;
|
|
|
|
|
if (distXY(intruder.x, intruder.y, bed.x, bed.y) > Math.max((bed.r || 18) * 2.2, 54)) continue;
|
|
|
|
|
const defender = sleepers.find(other => other && other !== intruder && !this.areParentChild?.(intruder, other) && !isFriendPair(intruder, other));
|
|
|
|
|
if (!defender) continue;
|
|
|
|
|
if (this.startForcedFight?.(defender, intruder)) conflicts += 1;
|
|
|
|
|
}
|
|
|
|
|
return conflicts;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-28 13:40:41 +09:00
|
|
|
blastZunchiFrom(x, y, radius, strength = 1, options = {}) {
|
2026-06-21 14:09:35 +09:00
|
|
|
let moved = 0;
|
2026-06-28 13:40:41 +09:00
|
|
|
const limit = Math.max(1, Number(options.limit || 180) || 180);
|
|
|
|
|
const effectLimit = Math.max(0, Number(options.effectLimit || 12) || 12);
|
2026-06-21 14:09:35 +09:00
|
|
|
for (const it of this.nearbyItems(x, y, radius + 60)) {
|
2026-06-28 13:40:41 +09:00
|
|
|
if (moved >= limit) break;
|
2026-06-21 14:09:35 +09:00
|
|
|
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;
|
2026-06-28 13:40:41 +09:00
|
|
|
if (moved < effectLimit) 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)" }));
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
if (moved) this.drawListDirty = true;
|
|
|
|
|
return moved;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
update(dt) {
|
2026-06-26 12:46:32 +09:00
|
|
|
const simulation = window.TarinaiSimulation;
|
|
|
|
|
if (!simulation?.update) throw new Error("TarinaiSimulation is not available; TarinaiWorldUpdatePhases cannot be reached");
|
|
|
|
|
return simulation.update(this, dt);
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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) => {
|
2026-06-24 21:20:53 +09:00
|
|
|
if (!obj) return "missing";
|
|
|
|
|
const safeW = Math.max(pad * 2 + 1, Number(this.w || 1000) || 1000);
|
|
|
|
|
const safeH = Math.max(pad * 2 + 1, Number(this.h || 720) || 720);
|
|
|
|
|
if (!Number.isFinite(obj.x)) obj.x = safeW * 0.5;
|
|
|
|
|
if (!Number.isFinite(obj.y)) obj.y = safeH * 0.5;
|
2026-06-21 14:09:35 +09:00
|
|
|
const minX = pad - radius;
|
2026-06-24 21:20:53 +09:00
|
|
|
const maxX = safeW - pad + radius;
|
2026-06-21 14:09:35 +09:00
|
|
|
const minY = pad - radius;
|
2026-06-24 21:20:53 +09:00
|
|
|
const maxY = safeH - pad + radius;
|
2026-06-21 14:09:35 +09:00
|
|
|
if (obj.x >= minX && obj.x <= maxX && obj.y >= minY && obj.y <= maxY) return "ok";
|
2026-06-24 21:20:53 +09:00
|
|
|
obj.x = clamp(obj.x, pad, safeW - pad);
|
|
|
|
|
obj.y = clamp(obj.y, pad, safeH - pad);
|
2026-06-21 14:09:35 +09:00
|
|
|
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);
|
2026-06-24 21:20:53 +09:00
|
|
|
if (result === "moved") changedItems = true;
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
for (const ef of this.effects || []) {
|
2026-06-24 21:20:53 +09:00
|
|
|
repairPoint(ef, ef.size || 12);
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
2026-06-24 18:49:17 +09:00
|
|
|
const safeW = Math.max(pad * 2 + 1, Number(this.w || 1000) || 1000);
|
|
|
|
|
const safeH = Math.max(pad * 2 + 1, Number(this.h || 720) || 720);
|
|
|
|
|
const repairTarinai = (t) => {
|
|
|
|
|
t.x = clamp(Number.isFinite(t.x) ? t.x : safeW * 0.5, pad, safeW - pad);
|
|
|
|
|
t.y = clamp(Number.isFinite(t.y) ? t.y : safeH * 0.5, pad, safeH - pad);
|
|
|
|
|
t.vx = 0;
|
|
|
|
|
t.vy = 0;
|
|
|
|
|
t.entryTimer = 0;
|
|
|
|
|
t.lastOutOfBoundsRepairAt = this.time || 0;
|
|
|
|
|
};
|
2026-06-21 14:09:35 +09:00
|
|
|
for (const t of this.tarinai || []) {
|
|
|
|
|
if (!t || t.dead) continue;
|
2026-06-24 21:20:53 +09:00
|
|
|
if (t.state === "sunbath" || (t.sunbathTimer || 0) > 0.04) {
|
2026-06-26 19:04:32 +09:00
|
|
|
if (!Number.isFinite(t.x)) t.x = Number.isFinite(t.sunbathAnchorX) ? t.sunbathAnchorX : pad;
|
|
|
|
|
if (!Number.isFinite(t.y)) t.y = Number.isFinite(t.sunbathAnchorY) ? t.sunbathAnchorY : pad;
|
|
|
|
|
t.sunbathAnchorX = clamp(t.x, pad, safeW - pad);
|
|
|
|
|
t.sunbathAnchorY = clamp(t.y, pad, safeH - pad);
|
2026-06-24 21:20:53 +09:00
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
const result = repairPoint(t, t.radius || 22);
|
2026-06-24 21:20:53 +09:00
|
|
|
if (result === "missing" || result === "moved") repairTarinai(t);
|
|
|
|
|
if (t.state === "sunbath" || (t.sunbathTimer || 0) > 0.04) {
|
2026-06-26 19:04:32 +09:00
|
|
|
t.sunbathAnchorX = clamp(Number.isFinite(t.x) ? t.x : (Number.isFinite(t.sunbathAnchorX) ? t.sunbathAnchorX : pad), pad, safeW - pad);
|
|
|
|
|
t.sunbathAnchorY = clamp(Number.isFinite(t.y) ? t.y : (Number.isFinite(t.sunbathAnchorY) ? t.sunbathAnchorY : pad), pad, safeH - pad);
|
2026-06-24 21:20:53 +09:00
|
|
|
t.x = t.sunbathAnchorX;
|
|
|
|
|
t.y = t.sunbathAnchorY;
|
|
|
|
|
t._lastSunbathDrawX = t.x;
|
|
|
|
|
t._lastSunbathDrawY = t.y;
|
|
|
|
|
t._lastValidX = t.x;
|
|
|
|
|
t._lastValidY = t.y;
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
let changedAnts = false;
|
|
|
|
|
for (const ant of this.ants || []) {
|
|
|
|
|
if (!ant || ant.dead) continue;
|
|
|
|
|
const result = repairPoint(ant, ant.kind === "queen" ? 18 : 10);
|
2026-06-24 21:20:53 +09:00
|
|
|
if (result === "missing") continue;
|
2026-06-21 14:09:35 +09:00
|
|
|
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();
|
2026-06-24 13:48:22 +09:00
|
|
|
this.markSpatialDirty?.("sanitize-out-of-bounds");
|
|
|
|
|
this.ensureSpatial?.("sanitize-out-of-bounds");
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateWeather(dt) {
|
2026-06-26 19:04:32 +09:00
|
|
|
if (window.TarinaiWeatherSystem?.enforceFixedWeather?.(this)) return;
|
2026-06-21 14:09:35 +09:00
|
|
|
this.nextWeatherChange -= dt;
|
|
|
|
|
if (this.nextWeatherChange > 0) return;
|
2026-06-21 22:29:00 +09:00
|
|
|
if (window.TarinaiWeatherSystem?.applyNextWeather) {
|
|
|
|
|
window.TarinaiWeatherSystem.applyNextWeather(this);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
const current = this.weather || "sunny";
|
2026-06-21 22:29:00 +09:00
|
|
|
const next = pick(current === "light_rain" ? ["sunny", "cloudy"] : ["sunny", "cloudy", "light_rain"]);
|
2026-06-21 14:09:35 +09:00
|
|
|
this.weather = next;
|
|
|
|
|
this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
|
2026-06-21 22:29:00 +09:00
|
|
|
if (next !== current) {
|
|
|
|
|
this.emit?.("weather:changed", { previous: current, next });
|
|
|
|
|
this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`);
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
})(typeof window !== "undefined" ? window : globalThis);
|