"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(force = false) { const nowScore = this.time || 0; if (!force && this.environmentScoreCache && nowScore < (this.nextEnvironmentScoreAt || 0)) return this.environmentScoreCache; 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); this.environmentScoreCache = { security, hygiene, happiness }; this.nextEnvironmentScoreAt = nowScore + 3.0; return this.environmentScoreCache; }, 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; }, hourOfDay(timeValue = this.time || 0) { return ((((timeValue || 0) / Math.max(1, CONFIG.dayLength || 120)) * 24) % 24 + 24) % 24; }, colonyMoodDefinition(id = "relaxed") { const defs = { calm: { id: "calm", label: "穏やか", description: "社交性が上がる", effects: { personality: { sociability: 0.42 } } }, prickly: { id: "prickly", label: "緊張", description: "攻撃性が上がる", effects: { personality: { aggression: 0.46 } } }, stinky: { id: "stinky", label: "不衛生", description: "開放性が下がる", effects: { personality: { openness: -0.44 } } }, weaklings: { id: "weaklings", label: "弱体化", description: "ずんちどれい以外のストレスが下がる", effects: { personality: {} } }, zunda_party: { id: "zunda_party", label: "ずんだ集中", description: "社交性と開放性が上がり、ストレスが下がる", effects: { personality: { sociability: 0.38, openness: 0.38 } } }, ball_party: { id: "ball_party", label: "ボール集中", description: "社交性と開放性が上がり、ストレスが下がる", effects: { personality: { sociability: 0.38, openness: 0.38 } } }, sun_party: { id: "sun_party", label: "日光浴傾向", description: "日光浴の頻度が上がる", effects: { personality: {} } }, ant_overrun: { id: "ant_overrun", label: "アリ増加", description: "アリを見るとパニックになりやすい", effects: { personality: {} } }, relaxed: { id: "relaxed", label: "安定", description: "標準状態", effects: { personality: {} } }, }; return defs[id] || defs.relaxed; }, 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 friendThreshold = typeof FRIEND_AFFINITY_THRESHOLD === "number" ? FRIEND_AFFINITY_THRESHOLD : 14; const rel = s.relationTo ? s.relationTo(t.id) : null; 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"; return ""; }, 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; }, 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) { const simulation = window.TarinaiSimulation; if (!simulation?.update) throw new Error("TarinaiSimulation is not available; TarinaiWorldUpdatePhases cannot be reached"); return simulation.update(this, dt); }, 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 "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; const minX = pad - radius; const maxX = safeW - pad + radius; const minY = pad - radius; const maxY = safeH - pad + radius; if (obj.x >= minX && obj.x <= maxX && obj.y >= minY && obj.y <= maxY) return "ok"; obj.x = clamp(obj.x, pad, safeW - pad); obj.y = clamp(obj.y, pad, safeH - 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 === "moved") changedItems = true; } for (const ef of this.effects || []) { repairPoint(ef, ef.size || 12); } 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; }; for (const t of this.tarinai || []) { if (!t || t.dead) continue; if (t.state === "sunbath" || (t.sunbathTimer || 0) > 0.04) { const fallbackX = Number.isFinite(t.sunbathAnchorX) ? t.sunbathAnchorX : (Number.isFinite(t._lastSunbathDrawX) ? t._lastSunbathDrawX : (Number.isFinite(t._lastValidX) ? t._lastValidX : pad)); const fallbackY = Number.isFinite(t.sunbathAnchorY) ? t.sunbathAnchorY : (Number.isFinite(t._lastSunbathDrawY) ? t._lastSunbathDrawY : (Number.isFinite(t._lastValidY) ? t._lastValidY : pad)); t.sunbathAnchorX = clamp(fallbackX, pad, safeW - pad); t.sunbathAnchorY = clamp(fallbackY, pad, safeH - pad); if (!Number.isFinite(t.x)) t.x = t.sunbathAnchorX; if (!Number.isFinite(t.y)) t.y = t.sunbathAnchorY; } const result = repairPoint(t, t.radius || 22); if (result === "missing" || result === "moved") repairTarinai(t); if (t.state === "sunbath" || (t.sunbathTimer || 0) > 0.04) { t.sunbathAnchorX = clamp(Number.isFinite(t.sunbathAnchorX) ? t.sunbathAnchorX : t.x, pad, safeW - pad); t.sunbathAnchorY = clamp(Number.isFinite(t.sunbathAnchorY) ? t.sunbathAnchorY : t.y, pad, safeH - pad); t.x = t.sunbathAnchorX; t.y = t.sunbathAnchorY; t.vx = 0; t.vy = 0; t._lastSunbathDrawX = t.x; t._lastSunbathDrawY = t.y; t._lastValidX = t.x; t._lastValidY = t.y; } } 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 === "missing") 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.markSpatialDirty?.("sanitize-out-of-bounds"); this.ensureSpatial?.("sanitize-out-of-bounds"); } }, updateWeather(dt) { this.nextWeatherChange -= dt; if (this.nextWeatherChange > 0) return; if (window.TarinaiWeatherSystem?.applyNextWeather) { window.TarinaiWeatherSystem.applyNextWeather(this); return; } const current = this.weather || "sunny"; const next = pick(current === "light_rain" ? ["sunny", "cloudy"] : ["sunny", "cloudy", "light_rain"]); this.weather = next; this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax); if (next !== current) { this.emit?.("weather:changed", { previous: current, next }); this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`); } } })); })(typeof window !== "undefined" ? window : globalThis);