From 2741b93dea66072461a92fe039deb23257e93d84 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Wed, 24 Jun 2026 21:20:53 +0900 Subject: [PATCH] lag r --- LAG_OPTIMIZATION_NOTES.md | 54 ++++ PATCH_NOTES.md | 27 ++ app_manifest.json | 2 +- assets/sounds/voice_007_sunbath_pokapoka.wav | Bin 0 -> 47660 bytes index.html | 138 +++++----- js/ants.js | 6 +- js/audio.js | 1 + js/data.js | 7 +- js/debug_tools.js | 4 +- js/items.js | 24 +- js/main.js | 19 +- js/performance_manager.js | 117 +++------ js/render.js | 10 +- js/snapshot_system.js | 4 +- js/sound_pack.js | 6 +- js/tarinai.js | 12 +- js/tarinai_item_effects.js | 6 +- js/tarinai_needs_items.js | 232 ++++++++++++----- js/tarinai_render.js | 252 ++++++++++++------- js/tarinai_social_move_life.js | 29 ++- js/ui_layout_dialogs.js | 5 +- js/version.js | 4 +- js/weather_system.js | 1 - js/world_ants_system.js | 55 ++-- js/world_combat_effects.js | 9 - js/world_family_social.js | 6 +- js/world_placement_log.js | 17 +- js/world_spatial_budget.js | 104 ++++---- js/world_update.js | 124 ++++++--- js/world_view.js | 20 +- service-worker.js | 2 +- 31 files changed, 751 insertions(+), 546 deletions(-) create mode 100644 LAG_OPTIMIZATION_NOTES.md create mode 100644 assets/sounds/voice_007_sunbath_pokapoka.wav diff --git a/LAG_OPTIMIZATION_NOTES.md b/LAG_OPTIMIZATION_NOTES.md new file mode 100644 index 0000000..b3ade34 --- /dev/null +++ b/LAG_OPTIMIZATION_NOTES.md @@ -0,0 +1,54 @@ +# Lag optimization pass 16.00.01 + +## Intent +Observation-game pacing allows delayed non-critical scans. This pass removes dynamic load-quality checks and moves broad scans to fixed, low-frequency schedules. + +## Changes + +- Removed dynamic load-based quality calculation from hot paths. + - `performance_manager.js` is now a compatibility shim only. + - No workload count over tarinai / ants / items / effects is performed by performance quality. + - Main loop, renderer, DPR resize, rain density, tarinai render decorations, social scan limits, and item scan limits no longer call load quality. + +- Tarinai decision cadence reduced. + - Normal AI/action resolution: about 1.0-1.35s per tarinai, staggered. + - Urgent AI state: about 0.35s. + - Need recalculation: about 1.0-1.35s normally. + - Environmental comfort scan: about 3.0-4.4s per tarinai, staggered. + +- Non-critical item updates are scheduled. + - Realtime: balls, pins, firecrackers, falling genkotsu, moving items. + - Grass: about 3.0-5.2s staggered. + - Zunchi lifecycle: about 2.4s unless moving. + - Duplicator: about 2.0s. + - Water: about 1.8s. + - Trace/splat: about 1.2s. + - Serving-food passive decay: about 5.0s. + - Fully static items are not updated every frame. + +- Spatial rebuilds are capped. + - `ensureSpatial()` now rebuilds at most once per frame. + - Movement or item changes after that use the previous grid until the next frame. + +- Other broad scans are delayed or cached. + - Bed conflict check: 3.2s. + - High-speed tarinai collision scan: 1.8s. + - Compaction: 2.5s. + - Item/effect count rebuild: 2.0s or structural change. + - Out-of-bounds sanitation: 5.0s. + - Environment score cache: 3.0s. + - Terrain periodic dirtying: 3.0s. + - Stats UI interval: 2.5s. + - Ant nest management/spawn sync: 3.0s. + +- ID/spatial replacements. + - Pushpin lodged owner lookup uses `liveTarinaiById()` instead of scanning tarinai. + - Loose pushpin hit testing uses `nearbyTarinai()` instead of full tarinai scan. + - Ant home nest lookup uses `itemById()`. + - Ant target choice uses `nearbyTarinai()`. + - Several tarinai target lookups use `liveTarinaiById()`. + +## Validation + +- `python3 scripts/regression_check.py` passed. +- `node --check` passed for all JavaScript files. diff --git a/PATCH_NOTES.md b/PATCH_NOTES.md index f5c2c7d..444b67a 100644 --- a/PATCH_NOTES.md +++ b/PATCH_NOTES.md @@ -1,3 +1,10 @@ +# 15.52.00 - 草上限復活・日光浴描画修正 + +- 草だけ上限を復活。上限到達後は、手動配置と自然繁殖の新規追加を止める。既存草は上限処理では削除しない。 +- 日光浴開始位置をアンカー化し、発生後に中央へ寄らないよう固定。 +- 日光浴スプライト描画を通常描画経路に一本化し、未ロード時は透明化せず読み込み済み候補または通常表情へフォールバック。 +- 到達不能だった日光浴専用直接描画メソッドと、未使用のアイテム上限メソッドを削除。 + # 15.24.53 - 排泄直後のずんちを通常個体が即座に食べないよう、ずんち専用の摂食保護時間を追加。 @@ -23,3 +30,23 @@ - 日光浴中のスプライトを、通常表情ロックとは別枠で固定。 - sunbath 系スプライトを初期ロード対象へ追加。 + + +## 15.40.00 - アイテム上限廃止 +- アイテム総数・種類別上限による自動削除を廃止。 +- ずんち生成時に古いずんちを押し出し削除する処理を削除。 +- 草・柵の配置数上限と、草の繁殖数上限を削除。 +- compactItems() はプレイヤー削除・腐敗・枯死・摂食などで dead になったアイテムだけを片付ける。 + + +## 15.56.00 +- 日光浴中は通常描画経路へフォールバックしないようにし、描画座標を本体座標に固定。 +- 日光浴終了後に sunbath sprite ID が通常描画へ残らないよう、visibleSpriteId と spriteLockUntil をリセット。 +- 日光浴スプライト切替時は未ロード画像へ切り替えず、準備済みの直前スプライトまたは通常スプライトを同座標で描画して透明フレームを避ける。 + +## 16.00.00 - Sunbath voice finish +- Added `assets/sounds/voice_007_sunbath_pokapoka.wav` from the provided sunbath voice asset. +- Registered `voice_sunbath` in `sound_pack.js`. +- Routed bubbles containing `ぽか` through `audio.bubble(text)` to play `voice_sunbath` at the same moment the bubble is spawned. +- Kept the existing sunbath rendering stabilization untouched. + diff --git a/app_manifest.json b/app_manifest.json index 4c39193..91565c7 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "15.24.53", + "version": "16.00.01", "css": [ "css/base.css", "css/layout.css", diff --git a/assets/sounds/voice_007_sunbath_pokapoka.wav b/assets/sounds/voice_007_sunbath_pokapoka.wav new file mode 100644 index 0000000000000000000000000000000000000000..be9482790c8c4183d4f1c5371cc356cd8e60400b GIT binary patch literal 47660 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 たりない観察 - - - - - + + + + +
@@ -183,69 +183,69 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/ants.js b/js/ants.js index 91e069b..eaf8e79 100644 --- a/js/ants.js +++ b/js/ants.js @@ -94,7 +94,8 @@ class AntActor { } homeNest() { - return (this.world?.items || []).find(it => it && !it.dead && it.id === this.homeId && it.type === "ant_nest") || null; + const nest = this.world?.itemById?.(this.homeId) || null; + return nest && nest.type === "ant_nest" ? nest : null; } adoptableNest() { @@ -165,7 +166,8 @@ class AntActor { // filter or rank candidates here while preserving sleep-disease hauling. let best = null; let bestScore = Infinity; - for (const t of this.world?.tarinai || []) { + const candidates = this.world?.nearbyTarinai?.(this.x, this.y, maxDist, true) || this.world?.tarinai || []; + for (const t of candidates) { if (!t || t.dead || t.insideNestBoxId || t.isZunchiSlave) continue; const d = distXY(this.x, this.y, t.x, t.y); if (d > maxDist) continue; diff --git a/js/audio.js b/js/audio.js index 6abcfbc..1e2ac20 100644 --- a/js/audio.js +++ b/js/audio.js @@ -298,6 +298,7 @@ const audio = { if (s.includes("Zzz")) return this.playSample("sfx_sleep", 1.25, "voice") || this.play("sfx_sleep", { category: "voice", minGap: 1.25 }); if (s.includes("\u306f\u3041") || s.includes("\u306f\u3042")) return this.playSample("voice_flee", 0.70, "voice") || this.play("sfx_damage_heavy", { category: "voice", minGap: 0.70 }); if (s.includes("\u306f\u3046") || s.includes("\u306f\u3045")) return this.playSample("voice_hau", 0.45, "voice") || this.play("sfx_wake", { category: "voice", minGap: 0.45 }); + if (s.includes("\u307d\u304b")) return this.playSample("voice_sunbath", 0.95, "voice") || this.play("sfx_heal", { category: "voice", minGap: 0.95 }); return false; }, eat() { if (!this.playSample("sfx_eat", 0.20, "voice")) this.play("sfx_eat", { category: "voice", minGap: 0.20 }); }, diff --git a/js/data.js b/js/data.js index b91fd99..fb02751 100644 --- a/js/data.js +++ b/js/data.js @@ -235,13 +235,9 @@ const NEEDS = ["\u98df\u3079\u7269", "\u4ef2\u9593", "\u7720\u308a", "\u52c7\u6c const CONFIG = { initialPopulation: 18, maxPopulation: Number.MAX_SAFE_INTEGER, - itemLimit: 260, - fenceLimit: 72, maxFenceCollisionChecks: 18, - zunchiLimit: 56, - traceLimit: 64, - splatLimit: 48, effectLimit: 78, + grassLimit: 360, worldPadding: 30, hungerPerMinute: 36.0, lonelinessPerMinute: 7.6, @@ -255,7 +251,6 @@ const CONFIG = { weatherChangeMin: 46, weatherChangeMax: 96, bubbleLimit: 32, - grassLimit: 360, highPopulationMode: 70, ultraPopulationMode: 120, }; diff --git a/js/debug_tools.js b/js/debug_tools.js index 898a7b8..cfbb02e 100644 --- a/js/debug_tools.js +++ b/js/debug_tools.js @@ -24,11 +24,11 @@ const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {}; const behaviorCounts = diag.behavior?.counts || {}; const topBehaviors = Object.entries(behaviorCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([id, n]) => `${id}:${n}`).join(", "); - const quality = diag.performance?.quality || perf.quality || {}; + const quality = {}; panel.textContent = [ `tarinai v${window.TARINAI_VERSION || "?"}`, `fps ${perf.fps ?? window.__tarinaiFps ?? "?"} perf ${perf.level ?? "?"}`, - `live ${diag.performance?.liveTarinai ?? quality.tarinaiCount ?? (w?.tarinai || []).length} qlevel ${quality.level ?? diag.performance?.level ?? "?"} pressure ${Number(diag.performance?.pressure ?? quality.pressure ?? 0).toFixed(2)} dpr ${diag.performance?.dpr || uiCache?.canvasDpr || "?"} maxDpr ${Number(quality.maxDpr ?? 0).toFixed ? Number(quality.maxDpr ?? 0).toFixed(2) : "?"}`, + `live ${diag.performance?.liveTarinai ?? (w?.tarinai || []).length} load-check removed dpr ${diag.performance?.dpr || uiCache?.canvasDpr || "?"}`, `items ${(w?.items || []).length} tarinai ${(w?.tarinai || []).length} ants ${(w?.ants || []).length}`, `effects ${(w?.effects || []).length}`, `terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0}`, diff --git a/js/items.js b/js/items.js index a12f7e0..dc5ea42 100644 --- a/js/items.js +++ b/js/items.js @@ -943,7 +943,8 @@ class Item { let best = null; let bestD = Infinity; const hitRange = Math.max(10, (this.r || 8) * 1.25); - for (const t of worldRef.tarinai) { + const candidates = worldRef.nearbyTarinai?.(this.x, this.y, hitRange + 32, true) || worldRef.tarinai; + for (const t of candidates) { if (!t || t.dead) continue; if (this.noStickTargetId && this.noStickTargetId === t.id && (this.noStickTargetUntil || 0) > now) continue; const d = distXY(this.x, this.y, t.x, t.y); @@ -1005,7 +1006,7 @@ class Item { } updateLodgedPushpin(dt, worldRef) { - const t = worldRef.tarinai?.find(o => o && !o.dead && o.id === this.pinTargetId) || null; + const t = worldRef.liveTarinaiById?.(this.pinTargetId) || null; if (!t) { this.detachPushpin(worldRef, "owner-lost"); return; @@ -1055,7 +1056,7 @@ class Item { } detachPushpin(worldRef, reason = "released") { - const t = worldRef?.tarinai?.find(o => o && o.id === this.pinTargetId) || null; + const t = worldRef?.liveTarinaiById?.(this.pinTargetId) || null; const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(this.type) : null; const wasOshibyo = Boolean(behavior?.blocksZunchi); const storedZunchi = wasOshibyo && t ? Math.max(0, Math.floor(t.oshiriByoZunchiStock || 0)) : 0; @@ -1140,8 +1141,8 @@ class Item { const growthRate = (0.006 + light * 0.004 + weatherBoost * 0.01 + waterNear * 0.014 + this.fertilityBoost * 0.014) * dt * (2 / 3); this.growth = clamp((this.growth ?? 0.25) + growthRate - dryStress * dt, 0, 0.88); this.health = clamp((this.health ?? 1) + dt * (waterNear > 0.05 || weather === "light_rain" ? 0.017 : -0.0015 - dryStress * 0.28), 0.18, 1); - const grassLimit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96); - if (this.growth >= 0.86 && (worldRef.itemCounts?.grass || 0) < grassLimit) { + const grassCanSpread = worldRef.canAddGrass ? worldRef.canAddGrass(1) : ((worldRef.itemCounts?.grass || 0) < (CONFIG.grassLimit ?? 360)); + if (this.growth >= 0.86 && grassCanSpread) { const spawned = this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 2); if (spawned > 0) this.growth = rand(0.52, 0.68); this.seedTimer = rand(9, 17); @@ -1157,15 +1158,15 @@ class Item { this.amount = clamp(this.growth * 120 * this.health, 0, 130); if (this.age > (this.lifeSpan || 640) * 1.05) this.amount -= dt * (1.2 + (this.wither || 0) * 24); this.seedTimer -= dt * (this.growth > 0.72 ? 1 : 0.25) * (2 / 3); - if (this.seedTimer <= 0 && this.growth > 0.64 && (worldRef.itemCounts?.grass || 0) < grassLimit) { + if (this.seedTimer <= 0 && this.growth > 0.64) { this.seedTimer = rand(12, 27); - if (Math.random() < 0.62 + this.fertilityBoost * 0.22) this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 1); + if ((worldRef.canAddGrass ? worldRef.canAddGrass(1) : ((worldRef.itemCounts?.grass || 0) < (CONFIG.grassLimit ?? 360))) + && Math.random() < 0.62 + this.fertilityBoost * 0.22) this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 1); } } seedAround(worldRef, fertileSource = null, fertile = false, maxNew = 1) { let made = 0; - const limit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96); - for (let n = 0; n < maxNew && (worldRef.itemCounts?.grass || 0) < limit; n++) { + for (let n = 0; n < maxNew && (worldRef.canAddGrass ? worldRef.canAddGrass(1) : ((worldRef.itemCounts?.grass || 0) < (CONFIG.grassLimit ?? 360))); n++) { const source = fertile && fertileSource ? fertileSource : this; const spot = worldRef.findGrassSproutSpot ? worldRef.findGrassSproutSpot(source, Boolean(fertile && fertileSource)) @@ -1174,7 +1175,8 @@ class Item { const sprout = new Item("grass", spot.x, spot.y); sprout.growth = rand(0.08, 0.18); sprout.amount = sprout.growth * 120; - worldRef.addItem?.(sprout, "grass-seed") || worldRef.items.push(sprout); + const added = worldRef.addItem?.(sprout, "grass-seed") || (!worldRef.addItem && worldRef.items.push(sprout) && sprout); + if (!added) break; made += 1; } return made; @@ -1219,7 +1221,7 @@ class Item { const styleNight = lightState.light < 0.42; const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55); if (isPinType(this.type) && this.pinState === "lodged") { - const owner = world?.tarinai?.find?.(o => o && !o.dead && o.id === this.pinTargetId) || null; + const owner = world?.liveTarinaiById?.(this.pinTargetId) || null; if (owner?.insideNestBoxId) return; } const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null; diff --git a/js/main.js b/js/main.js index 8df5582..d9ae4cd 100644 --- a/js/main.js +++ b/js/main.js @@ -3,9 +3,6 @@ let last = nowSec(); let statsTimer = 0; let fpsTimer = 0; let fpsFrames = 0; -let perfResizeTimer = 0; -let lastPerfLevel = 0; -let lastPerfMaxDpr = 0; window.__tarinaiFps = 0; const LOADING_SPRITES = [ @@ -85,26 +82,14 @@ function loop() { fpsFrames += 1; if (fpsTimer >= 0.5) { window.__tarinaiFps = Math.round(fpsFrames / Math.max(fpsTimer, 0.001)); - window.TarinaiPerformance?.updateFps?.(window.__tarinaiFps, world); + window.TarinaiPerformance?.updateFps?.(window.__tarinaiFps); fpsTimer = 0; fpsFrames = 0; } world.update(dt); render(); statsTimer += dt; - const quality = world.performanceQuality?.() || {}; - const perf = Number(quality.level ?? (world.performanceLevel ? world.performanceLevel() : 0)) || 0; - perfResizeTimer += rawDt; - if (perfResizeTimer > 1.5) { - const maxDpr = Number(quality.maxDpr || 0) || 0; - if (perf !== lastPerfLevel || Math.abs(maxDpr - lastPerfMaxDpr) > 0.08) { - lastPerfLevel = perf; - lastPerfMaxDpr = maxDpr; - resizeCanvas(); - } - perfResizeTimer = 0; - } - const statsInterval = world.performanceQuality?.().statsInterval ?? (perf >= 3 ? 1.20 : perf === 2 ? 0.92 : perf === 1 ? 0.68 : 0.48); + const statsInterval = 2.5; if (statsTimer > statsInterval) { renderStats(); statsTimer = 0; diff --git a/js/performance_manager.js b/js/performance_manager.js index 2710873..5b54975 100644 --- a/js/performance_manager.js +++ b/js/performance_manager.js @@ -1,100 +1,55 @@ "use strict"; +// Build 15.6.2-lag-pass: +// Dynamic load-based quality switching was removed. The old implementation +// counted tarinai/ants/items/effects from many hot paths, so the load check +// itself became a source of frame spikes. Keep a tiny compatibility object for +// legacy optional calls, but never scan the world and never change quality. class TarinaiPerformanceManager { constructor() { this.fps = 60; this.level = 0; this.lastLevel = 0; - this.samples = []; - this.sampleLimit = 20; - this.pressure = 0; } - updateFps(fps, worldRef = null) { + updateFps(fps) { const n = Number(fps); - if (!Number.isFinite(n) || n <= 0) return this.level; - this.samples.push(n); - if (this.samples.length > this.sampleLimit) this.samples.shift(); - const avg = this.samples.reduce((a, b) => a + b, 0) / this.samples.length; - this.fps = Math.round(avg); - this.pressure = this.computePressure(avg, worldRef); - const next = this.computeLevel(avg, worldRef); - if (next !== this.level) { - this.lastLevel = this.level; - this.level = next; - window.TarinaiEvents?.emit("performance:level", { level: this.level, previous: this.lastLevel, fps: this.fps, world: worldRef || null }); - } - return this.level; - } - - computeLevel(fps = this.fps, worldRef = null) { - const pressure = this.computePressure(fps, worldRef); - if (pressure >= 0.78) return 3; - if (pressure >= 0.54) return 2; - if (pressure >= 0.30) return 1; + if (Number.isFinite(n) && n > 0) this.fps = Math.round(n); return 0; } - computePressure(fps = this.fps, worldRef = null) { - const q = this.countWorkload(worldRef); - const n = Number(fps); - const fpsPressure = Number.isFinite(n) && n > 0 ? clamp((58 - n) / 38, 0, 1) : 0; - const workPressure = clamp(Math.log1p(q.workUnits / 42) / Math.log1p(260 / 42), 0, 1); - const sampleBoost = this.samples.length < 4 ? 0.84 : 1.0; - return clamp(Math.max(fpsPressure, workPressure * 0.78) * sampleBoost, 0, 1); + quality() { + return TarinaiPerformanceManager.STATIC_QUALITY; } - countWorkload(worldRef = null) { - let tarinaiCount = 0; - for (const t of worldRef?.tarinai || []) if (t && !t.dead) tarinaiCount += 1; - let antCount = 0; - for (const ant of worldRef?.ants || []) if (ant && !ant.dead) antCount += 1; - const itemCount = (worldRef?.items || []).length || 0; - const effectCount = (worldRef?.effects || []).length || 0; - const workUnits = tarinaiCount + antCount * 0.35 + itemCount * 0.08 + effectCount * 0.12; - return { tarinaiCount, antCount, itemCount, effectCount, workUnits }; - } - - quality(worldRef = null) { - const counts = this.countWorkload(worldRef); - const pressure = this.computePressure(this.fps, worldRef); - const level = Math.max(this.level, this.computeLevel(this.fps, worldRef)); - const density = clamp(1 - pressure * 0.82, 0.18, 1); - const antStride = Math.max(1, Math.round(1 + pressure * 2.4)); - const aiIntervalScale = 1 + pressure * 5.2; - const envIntervalScale = 1 + pressure * 4.8; - const socialScanLimit = Math.max(3, Math.round(14 - pressure * 10)); - const itemScanLimit = Math.max(5, Math.round(28 - pressure * 18)); - const collisionInterval = 0.04 + pressure * 0.24; - return { - fps: this.fps, - pressure, - level, - effectDensity: density, - tarinaiCount: counts.tarinaiCount, - antStride, - itemCount: counts.itemCount, - antCount: counts.antCount, - effectCount: counts.effectCount, - workUnits: counts.workUnits, - maxDpr: clamp(2.0 - pressure * 1.35, 0.65, 2.0), - effectLimit: Math.round(30 + density * 58), - aiIntervalScale, - envIntervalScale, - socialScanLimit, - itemScanLimit, - collisionInterval, - bedConflictInterval: 0.12 + pressure * 0.72, - statsInterval: 0.42 + pressure * 0.86, - terrainRedrawInterval: 0.62 + pressure * 0.72, - backgroundLightStep: Math.round(180 - pressure * 116), - uiUpdateInterval: 0.45 + pressure * 0.78, - }; - } - - debugSnapshot(worldRef = null) { - return { version: window.TARINAI_VERSION || "", fps: this.fps, level: this.level, quality: this.quality(worldRef) }; + debugSnapshot() { + return { version: window.TARINAI_VERSION || "", fps: this.fps, level: 0, quality: TarinaiPerformanceManager.STATIC_QUALITY }; } } +TarinaiPerformanceManager.STATIC_QUALITY = Object.freeze({ + fps: 60, + pressure: 0, + level: 0, + effectDensity: 1, + tarinaiCount: 0, + antStride: 2, + itemCount: 0, + antCount: 0, + effectCount: 0, + workUnits: 0, + maxDpr: 2, + effectLimit: 96, + aiIntervalScale: 1, + envIntervalScale: 1, + socialScanLimit: 8, + itemScanLimit: 16, + collisionInterval: 1.8, + bedConflictInterval: 3.2, + statsInterval: 1.2, + terrainRedrawInterval: 3.0, + backgroundLightStep: 180, + uiUpdateInterval: 1.2, +}); + window.TarinaiPerformance = window.TarinaiPerformance || new TarinaiPerformanceManager(); diff --git a/js/render.js b/js/render.js index de23a1f..d25b745 100644 --- a/js/render.js +++ b/js/render.js @@ -704,9 +704,7 @@ function drawAtmosphericLighting(ctx, w, h, lighting) { function drawRain(ctx, w, h, t) { if (world.weather !== "light_rain") return; ctx.save(); - const quality = world.performanceQuality?.() || {}; - const density = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1); - const count = Math.max(20, Math.round(138 * density)); + const count = 96; const angle = -0.32; // Rain visual effect: diagonal streaks, separate from fallen water item sprites. const slant = Math.tan(angle); for (let i = 0; i < count; i++) { @@ -730,7 +728,7 @@ function drawRain(ctx, w, h, t) { ctx.moveTo(x - dx * 0.5, y - dy * 0.5); ctx.lineTo(x + dx * 0.5, y + dy * 0.5); ctx.stroke(); - if (density > 0.25 && i % 4 === 0) { + if (i % 4 === 0) { ctx.globalAlpha = 0.13 * alpha; ctx.strokeStyle = "rgba(255,255,255,0.70)"; ctx.lineWidth = 0.7; @@ -796,9 +794,7 @@ function render() { } ctx.restore(); - const quality = world.performanceQuality?.() || {}; - const density = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1); - if (density > 0.45) drawLightRays(ctx, screenW, screenH, lighting); + drawLightRays(ctx, screenW, screenH, lighting); const visibleRect = visibleWorldRect(world, 180); const terrainLayer = ensureTerrainCache(world, lighting); diff --git a/js/snapshot_system.js b/js/snapshot_system.js index a365c48..f66b5b9 100644 --- a/js/snapshot_system.js +++ b/js/snapshot_system.js @@ -264,7 +264,7 @@ function itemExtra(item, tarinaiIndex = new Map()) { const type = item?.type || ""; if (item?.isStructure) return ["S", q(item.hp, 10), q(item.maxHp, 10), q(item.attachment, 10), q(item.usedCount, 1), tarinaiIndex.get(item.ownerId) ?? -1, item.carriedById ? (tarinaiIndex.get(item.carriedById) ?? -1) : -1, item.onHead ? 1 : 0, item.plushieSpriteId || ""]; - if (type === "grass") return [q(item.growth, 1000), q(item.health, 1000), q(item.seedTimer, 10), q(item.eatenAmount, 10), q(item.fertilityBoost, 1000), q(item.lifeSpan, 10), q(item.wither, 1000)]; + if (type === "grass") return [q(item.growth, 1000), q(item.health, 1000), q(item.seedTimer, 10), q(item.eatenAmount, 10), q(item.fertilityBoost, 1000), q(item.lifeSpan, 10), q(item.wither, 1000), item.grassLimitExempt || item.manualGrass || item.placedByPlayer ? 1 : 0]; if (type === "bed") return [q(item.comfort, 1000), q(item.wear, 1000)]; if (type === "signboard") return [item.text || ""]; if (type === "duplicator") return [item.storedFoodType || "", item.storedFoodLabel || ""]; @@ -306,7 +306,7 @@ return; } if (type === "grass") { - item.growth = u(extra[0], 1000); item.health = u(extra[1], 1000); item.seedTimer = u(extra[2], 10); item.eatenAmount = u(extra[3], 10); item.fertilityBoost = u(extra[4], 1000); item.lifeSpan = u(extra[5], 10); item.wither = u(extra[6], 1000); + item.growth = u(extra[0], 1000); item.health = u(extra[1], 1000); item.seedTimer = u(extra[2], 10); item.eatenAmount = u(extra[3], 10); item.fertilityBoost = u(extra[4], 1000); item.lifeSpan = u(extra[5], 10); item.wither = u(extra[6], 1000); item.grassLimitExempt = !!extra[7]; item.manualGrass = !!extra[7]; } else if (type === "bed") { item.comfort = u(extra[0], 1000); item.wear = u(extra[1], 1000); } else if (type === "signboard") { diff --git a/js/sound_pack.js b/js/sound_pack.js index 40682ea..e67e8ab 100644 --- a/js/sound_pack.js +++ b/js/sound_pack.js @@ -3,7 +3,7 @@ (function (global) { // External audio replacement point. Add files and edit paths here; audio.js consumes this pack at startup. global.TARINAI_SOUND_PACK = Object.freeze({ - version: "1.1", + version: "1.2", categoryGain: { voice: 3.2, notify: 18.0, ops: 16.0 }, idGain: { sfx_notify: 2.60, @@ -30,6 +30,7 @@ sfx_ant_die: 0.14, sfx_notify: 0.10, sfx_log_push: 0.10, + voice_sunbath: 0.95, }, samplePaths: { voice_hau: "assets/sounds/voice_001_hau.wav", @@ -38,6 +39,7 @@ sfx_eat: "assets/sounds/voice_004_eat.wav", voice_stress: "assets/sounds/voice_005_stress.wav", sfx_sleep: "assets/sounds/voice_006_sleep.wav", + voice_sunbath: "assets/sounds/voice_007_sunbath_pokapoka.wav", sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3" }, categories: { @@ -46,7 +48,7 @@ ops: "\u64cd\u4f5c" }, soundMap: { - voice_hau: "voice", voice_flee: "voice", voice_poop: "voice", voice_stress: "voice", + voice_hau: "voice", voice_flee: "voice", voice_poop: "voice", voice_stress: "voice", voice_sunbath: "voice", sfx_eat: "voice", sfx_sleep: "voice", sfx_wake: "voice", sfx_birth: "voice", sfx_death: "voice", sfx_damage_soft: "voice", sfx_damage_heavy: "voice", sfx_fight_hit: "voice", sfx_fight_finish: "voice", sfx_disease: "voice", sfx_heal: "voice", sfx_ant_drag: "voice", sfx_ant_die: "voice", sfx_queen_ant: "voice", diff --git a/js/tarinai.js b/js/tarinai.js index fb0f4a2..c2eee89 100644 --- a/js/tarinai.js +++ b/js/tarinai.js @@ -38,6 +38,11 @@ class Tarinai { constructor(world, opts = {}) { const growthAtConstruct = generationOpt > 1 ? clamp(((this.world?.time || 0) - this.birthTime) / Math.max(1, CONFIG.childGrowTime || 120), 0, 1) : 1; this.x = opts.x ?? rand(100, world.w - 100); this.y = opts.y ?? rand(100, world.h - 100); + this._lastValidX = Number.isFinite(this.x) ? this.x : null; + this._lastValidY = Number.isFinite(this.y) ? this.y : null; + this._lastSunbathDrawX = Number.isFinite(opts._lastSunbathDrawX) ? opts._lastSunbathDrawX : null; + this._lastSunbathDrawY = Number.isFinite(opts._lastSunbathDrawY) ? opts._lastSunbathDrawY : null; + this._activeSunbathSpriteId = typeof opts._activeSunbathSpriteId === "string" ? opts._activeSunbathSpriteId : ""; this.vx = opts.vx ?? rand(-12, 12); this.vy = opts.vy ?? rand(-12, 12); this.impulseVx = opts.impulseVx ?? 0; @@ -123,6 +128,8 @@ class Tarinai { constructor(world, opts = {}) { this.sunbathCooldown = opts.sunbathCooldown ?? 0; this.sunbathFrameTimer = opts.sunbathFrameTimer ?? 0; this.sunbathSpriteVariant = opts.sunbathSpriteVariant ?? 1; + this.sunbathAnchorX = Number.isFinite(opts.sunbathAnchorX) ? opts.sunbathAnchorX : null; + this.sunbathAnchorY = Number.isFinite(opts.sunbathAnchorY) ? opts.sunbathAnchorY : null; this.intimidateTimer = opts.intimidateTimer ?? 0; this.intimidateTargetId = opts.intimidateTargetId || null; this.intimidatedTimer = opts.intimidatedTimer ?? 0; @@ -174,7 +181,10 @@ class Tarinai { constructor(world, opts = {}) { this.fightTargetId = opts.fightTargetId || null; this.fightTargetIds = Array.isArray(opts.fightTargetIds) ? [...opts.fightTargetIds] : (this.fightTargetId ? [this.fightTargetId] : []); this.nextHeadbutt = opts.nextHeadbutt ?? 0; - this.digest = opts.digest ?? rand(0, 2.4); + const savedDigest = Number(opts.digest); + this.digest = Number.isFinite(savedDigest) + ? (savedDigest > 3 ? Math.min(2.99, savedDigest / 12.4 * 3) : Math.max(0, savedDigest)) + : 0; this.poopCount = opts.poopCount ?? 0; this.oshiriByoZunchiStock = opts.oshiriByoZunchiStock ?? 0; this.stretchTimer = opts.stretchTimer ?? 0; diff --git a/js/tarinai_item_effects.js b/js/tarinai_item_effects.js index 1beb5d7..efd8d49 100644 --- a/js/tarinai_item_effects.js +++ b/js/tarinai_item_effects.js @@ -285,10 +285,8 @@ this.poopCount = (this.poopCount || 0) + 1; this.digest = Math.max(0, (this.digest || 0) - 3.0); const now = this.world?.time || 0; - if (now >= (this.nextPoopBubbleAt || -999)) { - this.nextPoopBubbleAt = now + 2.2; - this.bubble("\u3076\u308a\u3085\u3063", 1.2, ITEM_EFFECT_COLORS.laxative); - } + this.nextPoopBubbleAt = now + 2.2; + this.bubble("\u3076\u308a\u3085\u3063", 1.2, ITEM_EFFECT_COLORS.laxative, { force: true }); if ((this.world.time || 0) > (this.nextLaxativeLogAt || -999)) { this.nextLaxativeLogAt = (this.world.time || 0) + 9; this.world.log?.(`${this.name}\u306f\u4e0b\u5264\u306e\u52b9\u679c\u3067\u305a\u3093\u3061\u3092\u653e\u51fa\u3057\u305f\u3002`, "accident", { participants: [this] }); diff --git a/js/tarinai_needs_items.js b/js/tarinai_needs_items.js index 6f121e8..915fe72 100644 --- a/js/tarinai_needs_items.js +++ b/js/tarinai_needs_items.js @@ -262,7 +262,10 @@ function scoreNeedForAction(needs, tarinai, world, need) { let score = value; if (need === "safety") score += 14; else if (need === "health") score += 8; - else if (need === "food") score += 3; + else if (need === "food") { + const hunger = Number(tarinai?.hunger || 0) || 0; + score += 3 + Math.max(0, hunger - 70) * 0.55 + Math.max(0, hunger - 92) * 0.95; + } if ((typeof getTarinaiBehaviorNeed === "function" ? getTarinaiBehaviorNeed(tarinai) : tarinai?.behavior?.need) === need) score += 10; const cooldown = Number(tarinai?.actionCooldowns?.[need] || 0) || 0; if (cooldown > 0) score -= Math.min(22, cooldown * 2.8); @@ -464,7 +467,7 @@ function currentBehaviorTarget(tarinai, behavior = null) { if (tarinai.target && !tarinai.target.dead && (!b.targetId || behaviorTargetId(tarinai.target) === b.targetId)) return tarinai.target; if (!b.targetId) return null; const id = b.targetId; - return (tarinai.world?.tarinai || []).find(o => o && !o.dead && o.id === id) + return tarinai.world?.liveTarinaiById?.(id) || tarinai.world?.itemById?.(id) || (tarinai.world?.items || []).find(item => item && !item.dead && item.id === id) || null; @@ -492,7 +495,7 @@ function nearestForcedFightTarget(tarinai, world, maxDist = 720) { function resolveForcedBehaviorTarget(tarinai, world, entry = {}, action = null) { if (!tarinai || !world) return null; if (entry.targetId) { - const found = (world.tarinai || []).find(o => o && !o.dead && o.id === entry.targetId) + const found = world.liveTarinaiById?.(entry.targetId) || world.itemById?.(entry.targetId) || (world.items || []).find(item => item && !item.dead && item.id === entry.targetId) || null; @@ -899,6 +902,8 @@ function isDuplicatorFoodSource(item) { return !!item && item.type === "duplicator"; } +const NORMAL_POOP_MEAL_THRESHOLD = 3; + function duplicatorPoopMultiplier(foodType = "") { if (foodType === "sweet") return 0.035; if (foodType === "food") return 0.040; @@ -906,6 +911,13 @@ function duplicatorPoopMultiplier(foodType = "") { return 0.042; } +function normalPoopUnitsFor(foodType = "", amount = 0, options = {}) { + const type = String(foodType || ""); + if (type === "laxative" || options.laxative === true) return 0; + if (typeof isServingFoodType === "function" && isServingFoodType(type)) return 1; + const value = Math.max(0, Number(amount) || 0); + return value / 18; +} function effectiveFoodRoleMatches(item, role) { const type = effectiveFoodTypeForItem(item); if (!type) return false; @@ -958,7 +970,11 @@ function foodPriorityRank(tarinai, item) { if (type === "fight_mochi") return 2600 + Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 4; if (type === "sleep_drug") return need === "sleep" ? 3600 : 2450; if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return 3000 + Math.max(0, healthNeed - 45) * 5; - if (type === "grass") return 2000; + if (type === "grass") { + const hunger = Number(tarinai?.hunger || 0) || 0; + const foodNeed = Number(tarinai?.needRaw?.food ?? tarinai?.needs?.food ?? 0) || 0; + return 2000 + Math.max(0, hunger - 64) * 18 + Math.max(0, foodNeed - 58) * 12; + } if (type === "zunchi") return 1000 + (tarinai?.isZunchiSlave ? 250 : 0); if (type === "food" || type === "ant_corpse") return 3000; if (typeof isServingFoodType === "function" && isServingFoodType(type)) return 3000; @@ -1014,7 +1030,10 @@ function zunchiEatingBlocked(tarinai, item, world) { function findNearestItemWithRole(world, tarinai, role, maxDist = 520) { let best = null, bestScore = Infinity; const hunger = Number(tarinai?.hunger || 0) || 0; - for (const item of world?.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || world?.items || []) { + const candidates = ((role === "food" || role === "drink" || role === "medicine") && world?.nearbyFood) + ? world.nearbyFood(tarinai.x, tarinai.y, maxDist, true) + : (world?.nearbyItems?.(tarinai.x, tarinai.y, maxDist, true) || world?.items || []); + for (const item of candidates) { if (!roleMatches(item, role)) continue; if (tarinai?.shouldAvoidTarget?.(item)) continue; const d = item.type === "duplicator" ? foodInteractionDistance(tarinai, item) : dist(tarinai, item); @@ -1120,7 +1139,7 @@ function conflictTargetFor(tarinai, world, minScore = 34) { const evaluated = evaluateConflictUrge(tarinai, world, 118); if (evaluated && evaluated.score >= minScore) return evaluated; const id = tarinai?.conflictTargetId || ""; - const target = id ? (world?.tarinai || []).find(o => o && !o.dead && o.id === id) : null; + const target = id ? world?.liveTarinaiById?.(id) : null; if (!target || !world?.canFightPair?.(tarinai, target)) return null; const d = dist(tarinai, target); const carried = Math.max(0, Number(tarinai.conflictUrge || 0) || 0) - Math.max(0, d - 70) * 0.28; @@ -1324,12 +1343,9 @@ function updateNeeds(tarinai, world, dt = 0) { } function tarinaiNeedCalcInterval(tarinai, world) { - const perf = world?.performanceLevel ? Number(world.performanceLevel() || 0) : 0; - const pop = Array.isArray(world?.tarinai) ? world.tarinai.length : 0; - const base = 0.42 + Math.min(0.34, pop * 0.0022) + perf * 0.16; - const jitter = typeof stableUnit === "function" ? stableUnit(tarinai?.familyKey || tarinai?.id || "tarinai", "need-calc-interval") * 0.32 : Math.random() * 0.24; + const jitter = typeof stableUnit === "function" ? stableUnit(tarinai?.familyKey || tarinai?.id || "tarinai", "need-calc-interval") * 0.35 : Math.random() * 0.30; const urgent = (tarinai?.fightTimer || 0) > 0.04 || (tarinai?.birthRitualTimer || 0) > 0.04 || (tarinai?.eatTimer || 0) > 0.04 || (tarinai?.sunbathTimer || 0) > 0.04 || forcedBehaviorQueueOf(tarinai).length > 0; - return clamp(urgent ? 0.24 + jitter * 0.34 : base + jitter, 0.22, 1.10); + return urgent ? (0.55 + jitter * 0.25) : (1.0 + jitter); } function shouldForceNeedRecalc(tarinai, world) { @@ -1595,7 +1611,7 @@ function createPanicEscapeActionSpec() { weight: 62, timerBacked: true, selectTarget(t, world) { - return t.lastNeedShockBreaker || findNearbyDanger(world, t, 240) || ((t.defeatedById && world?.tarinai) ? world.tarinai.find(o => o.id === t.defeatedById && !o.dead) : null); + return t.lastNeedShockBreaker || findNearbyDanger(world, t, 240) || ((t.defeatedById && world?.tarinai) ? world.liveTarinaiById?.(t.defeatedById) : null); }, canStart(t, world, ctx = {}) { return (Number(t?.needRaw?.safety ?? t?.needs?.safety ?? 0) >= 58) @@ -1950,7 +1966,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) { t.applyFoodHungerRelief ? t.applyFoodHungerRelief(bite * 0.62) : (t.hunger -= bite * 0.62); t.recoverHealth?.(bite * 0.12); applyNeedShock(t, { safety: bite * 0.12 }); - t.makePoop?.(bite * 0.08); + t.makePoop?.(normalPoopUnitsFor("ant_corpse", bite)); return true; } @@ -1972,7 +1988,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) { if (t.fightDisease) t.recoverFightDisease?.("\u305a\u3093\u3060\u9905\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f"); } const fromDuplicator = isDuplicatorFoodSource(target); - t.makePoop?.(eating * (fromDuplicator ? duplicatorPoopMultiplier(foodType) : (foodType === "sweet" ? 0.18 : 0.20))); + t.makePoop?.(normalPoopUnitsFor(foodType || target.type, eating)); if (fromDuplicator) { t.mealCooldownUntil = Math.max(t.mealCooldownUntil || 0, (world.time || 0) + 1.2); t.intentLockTimer = Math.max(0, Math.min(t.intentLockTimer || 0, 0.18)); @@ -1991,7 +2007,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) { t.applyFoodHungerRelief ? t.applyFoodHungerRelief(bite * 0.46) : (t.hunger -= bite * 0.46); t.recoverHealth?.(bite * 0.18); applyNeedRelief(t, { food: -2, fulfill: -1 }); - t.makePoop?.(bite * 0.10); + t.makePoop?.(normalPoopUnitsFor("grass", bite)); return true; } @@ -2009,7 +2025,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) { t.applyFoodHungerRelief ? t.applyFoodHungerRelief(bite * (t.isZunchiSlave ? 0.66 : 0.18)) : (t.hunger -= bite * (t.isZunchiSlave ? 0.66 : 0.18)); t.recoverHealth?.(bite * (t.isZunchiSlave ? 0.10 : 0.02)); applyNeedShock(t, { health: bite * (t.isZunchiSlave ? 0.2 : 1.2), safety: bite * (t.isZunchiSlave ? 0.1 : 0.6) }); - t.makePoop?.(bite * 0.08); + t.makePoop?.(normalPoopUnitsFor("zunchi", bite)); return true; } return false; @@ -2109,7 +2125,7 @@ function updateMateBehavior(t, world, dt) { function updateFightBehavior(t, world, dt) { if (!t || !world) return false; if ((t.fightTimer || 0) > 0.04) { - const rival = currentBehaviorTarget(t) || (t.fightTargetId ? world.tarinai.find(o => o && !o.dead && o.id === t.fightTargetId) : null); + const rival = currentBehaviorTarget(t) || (t.fightTargetId ? world.liveTarinaiById?.(t.fightTargetId) : null); t.setActionState?.("fight", { target: rival || t.target, reason: rival?.name ? `${rival.name}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` : "\u55a7\u5629\u3057\u3066\u3044\u308b", sleeping: false }); return true; } @@ -2367,8 +2383,14 @@ function choosePriorityNeedAction(needs, tarinai, world) { else if (action.id === "build_plushie") bonus += Number(p.plushie || 0) * 1.8 + Number(p.material || 0); else if (action.id === "play" || action.id === "wander_lightly") bonus += Number(p.boredom || 0) * 1.2; } - const priority = actionPriority(action); + let priority = actionPriority(action); const needValue = Number(needs?.[need] || 0) || 0; + if (action.id === "eat_food") { + const hunger = Number(tarinai?.hunger || 0) || 0; + if (hunger >= 98 || needValue >= 88) priority = Math.max(priority, 120); + else if (hunger >= 88 || needValue >= 78) priority = Math.max(priority, 98); + else if (hunger >= 78 || needValue >= 70) priority = Math.max(priority, 82); + } const urgency = Math.max(0, needValue - needThreshold(need, "start")); return { action, priority, need, needValue, urgency, bonus, score: priority * 1000 + needValue * 3 + (Number(action.weight || 1) + bonus) }; }); @@ -2378,7 +2400,7 @@ function choosePriorityNeedAction(needs, tarinai, world) { // \u539f\u5247\u306f\u884c\u52d5\u512a\u5148\u9806\u4f4d\u3002\u305f\u3060\u3057\u3001\u4f4e\u4f4d\u6b32\u6c42\u304c\u5371\u6a5f\u7684\u306b\u9ad8\u3044\u5834\u5408\u3060\u3051\u3001\u305d\u306e\u6b32\u6c42\u884c\u52d5\u3092\u9078\u3073\u3001 // \u8868\u793a\u6587\u306f\u300c\u7720\u305f\u3044\u3051\u3069\u3001\u98df\u3079\u7269\u3092\u63a2\u3057\u3066\u3044\u308b\u3002\u300d\u306e\u3088\u3046\u306b\u5bfe\u7acb\u3092\u660e\u793a\u3059\u308b\u3002 const crisis = weighted - .filter(e => e.needValue >= 88 && e.urgency >= 24 && (selectedEntry.priority - e.priority) <= 36) + .filter(e => e.needValue >= 88 && e.urgency >= 24 && ((selectedEntry.priority - e.priority) <= 36 || e.action?.id === "eat_food")) .sort((a, b) => (b.needValue - a.needValue) || (b.urgency - a.urgency) || (b.score - a.score))[0] || null; if (crisis && crisis.needValue >= selectedEntry.needValue + 16) selectedEntry = crisis; @@ -2693,13 +2715,18 @@ function protectSleepSession(tarinai, world, needs) { spriteId() { const now = this.world?.time || 0; - if (this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04) { + const sunbathActive = this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04; + if (sunbathActive) { this.preloadSunbathSprites?.(); const sunbathId = this.sunbathSpriteId ? this.sunbathSpriteId() : (String(this.sunbathSpriteVariant || "").startsWith("sunbath") ? this.sunbathSpriteVariant : "sunbath_1"); this.visibleSpriteId = sunbathId; - this.spriteLockUntil = now + 0.55; + this.spriteLockUntil = 0; return sunbathId; } + if (String(this.visibleSpriteId || "").startsWith("sunbath")) { + this.visibleSpriteId = ""; + this.spriteLockUntil = 0; + } const next = this.rawSpriteId(); const isCurrentIntimidator = this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04; const isCurrentIntimidated = !isCurrentIntimidator && this.intimidatedTimer > 0.04; @@ -2753,19 +2780,50 @@ function protectSleepSession(tarinai, world, needs) { return ""; }, - randomSunbathSpriteId(exclude = "") { + isSunbathSpriteReady(id = "") { + if (!id) return false; + if (typeof isImageReady === "function") return isImageReady(id); + const img = typeof images !== "undefined" ? images.get(id) : globalThis.TarinaiAssets?.images?.get?.(id); + return !!(img && img.complete && (img.naturalWidth || img.width)); + }, + + randomSunbathSpriteId(exclude = "", options = {}) { const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"]; if (!ids.length) return "sunbath_1"; const normalizedExclude = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(exclude) : String(exclude || ""); - const choices = ids.length > 1 ? ids.filter(id => id !== normalizedExclude) : ids; - return choices[Math.floor(Math.random() * choices.length)] || ids[0] || "sunbath_1"; + let candidates = ids; + if (options.readyOnly) { + const ready = ids.filter(id => this.isSunbathSpriteReady ? this.isSunbathSpriteReady(id) : true); + if (ready.length) candidates = ready; + else return (this._lastRenderableSunbathSpriteId && ids.includes(this._lastRenderableSunbathSpriteId)) + ? this._lastRenderableSunbathSpriteId + : ((normalizedExclude && ids.includes(normalizedExclude)) ? normalizedExclude : (ids[0] || "sunbath_1")); + } + const choices = candidates.length > 1 ? candidates.filter(id => id !== normalizedExclude) : candidates; + return choices[Math.floor(Math.random() * choices.length)] || candidates[0] || ids[0] || "sunbath_1"; }, sunbathSpriteId() { const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"]; - const current = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || ""); - if (!current || !ids.includes(current)) this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId() : "sunbath_1"; - return this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || "sunbath_1"); + let current = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || ""); + if (!current || !ids.includes(current)) { + this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId("", { readyOnly: true }) : "sunbath_1"; + current = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || "sunbath_1"); + } + if (!this.isSunbathSpriteReady?.(current)) { + const last = this._lastRenderableSunbathSpriteId; + if (last && ids.includes(last) && this.isSunbathSpriteReady?.(last)) { + this.sunbathSpriteVariant = last; + current = last; + } else { + const ready = ids.find(id => this.isSunbathSpriteReady?.(id)); + if (ready) { + this.sunbathSpriteVariant = ready; + current = ready; + } + } + } + return current || "sunbath_1"; }, isSunbathTime() { @@ -2784,9 +2842,11 @@ function protectSleepSession(tarinai, world, needs) { if ((this.sunbathCooldown || 0) > 0) return false; if (this.stuckPushpinId || this.currentLodgedPin?.()) return false; if (["fight", "panic", "cursor_enemy", "eat", "seek_food", "seek_water", "seek_bed", "sleep", "birth_ritual", "ant_attack", "intimidate", "ant_intimidate"].includes(this.state)) return false; - if (this.hunger > (options.leisure ? 58 : 68) || this.energy < (options.leisure ? 28 : 18)) return false; + if (this.hunger > (options.leisure ? 62 : 72) || this.energy < (options.leisure ? 24 : 16)) return false; const healthNeed = Number(this.needRaw?.health ?? this.needs?.health ?? 0) || 0; - return options.leisure ? healthNeed < needThreshold("health", "start") : healthNeed >= needThreshold("health", "start"); + const sick = this.zunchiDisease || this.sleepDisease || this.explosionDisease || this.fightDisease; + if (sick && healthNeed >= 34) return true; + return options.leisure ? healthNeed < needThreshold("health", "start") : healthNeed >= Math.max(38, needThreshold("health", "start") - 12); }, tryRecoverBySunbath() { @@ -2819,8 +2879,16 @@ function protectSleepSession(tarinai, world, needs) { const pad = Math.max(28, CONFIG.worldPadding || 30); const safeW = Math.max(pad * 2 + 1, Number(this.world?.w || 1000) || 1000); const safeH = Math.max(pad * 2 + 1, Number(this.world?.h || 720) || 720); - this.x = clamp(Number.isFinite(this.x) ? this.x : safeW * 0.5, pad, safeW - pad); - this.y = clamp(Number.isFinite(this.y) ? this.y : safeH * 0.5, pad, safeH - pad); + const startX = Number.isFinite(this.x) ? this.x : (Number.isFinite(this._lastValidX) ? this._lastValidX : pad); + const startY = Number.isFinite(this.y) ? this.y : (Number.isFinite(this._lastValidY) ? this._lastValidY : pad); + this.x = clamp(startX, pad, safeW - pad); + this.y = clamp(startY, pad, safeH - pad); + this.sunbathAnchorX = this.x; + this.sunbathAnchorY = this.y; + this._lastSunbathDrawX = this.x; + this._lastSunbathDrawY = this.y; + this._lastValidX = this.x; + this._lastValidY = this.y; this.entryTimer = 0; const reason = options.leisure ? "\u5c11\u3057\u843d\u3061\u7740\u3044\u3066\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b" : "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b"; this.setActionState?.("sunbath", { target: null, reason, sleeping: false }); @@ -2828,9 +2896,11 @@ function protectSleepSession(tarinai, world, needs) { this.sunbathTimer = Math.max(6, (CONFIG.dayLength || 120) / 12); this.sunbathCooldown = 0; this.sunbathFrameTimer = 0.5; - this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId() : `sunbath_${1 + Math.floor(Math.random() * 3)}`; + this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId("", { readyOnly: true }) : `sunbath_${1 + Math.floor(Math.random() * 3)}`; this.sunbathSpriteFrameKey = Math.floor((this.world?.time || 0) / 0.5); - this.visibleSpriteId = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant; + this._activeSunbathSpriteId = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant; + this._lastRenderableSunbathSpriteId = this._activeSunbathSpriteId; + this.visibleSpriteId = this._activeSunbathSpriteId; this.spriteLockUntil = 0; this.sunbathRecoveryChecked = false; this.vx = 0; @@ -2847,16 +2917,29 @@ function protectSleepSession(tarinai, world, needs) { this.vx = 0; this.vy = 0; if ((this.sunbathFrameTimer || 0) <= 0) { - const prev = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant; - this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId(prev) : `sunbath_${1 + Math.floor(Math.random() * 3)}`; - this.visibleSpriteId = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant; - this.spriteLockUntil = (this.world?.time || 0) + 0.55; + const prev = this._activeSunbathSpriteId || (this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant); + const next = this.randomSunbathSpriteId ? this.randomSunbathSpriteId(prev, { readyOnly: true }) : `sunbath_${1 + Math.floor(Math.random() * 3)}`; + if (next && next !== prev && (!this.isSunbathSpriteReady || this.isSunbathSpriteReady(next))) { + this._activeSunbathSpriteId = next; + this._lastRenderableSunbathSpriteId = next; + this.sunbathSpriteVariant = next; + this.visibleSpriteId = next; + this.spriteLockUntil = 0; + } else { + this.sunbathSpriteVariant = prev; + this.visibleSpriteId = prev; + } this.sunbathFrameTimer = 0.5; } this.energy = clamp((this.energy || 0) + dt * 0.03, 0, 100); if (!this.isSunbathTime() || this.hunger > 82 || this.energy < 8 || this.state === "panic" || this.stuckPushpinId || this.currentLodgedPin?.()) this.sunbathTimer = 0; if ((this.sunbathTimer || 0) <= 0 && this.state === "sunbath") { this.finishSunbath?.(); + this._activeSunbathSpriteId = ""; + this.visibleSpriteId = ""; + this.spriteLockUntil = 0; + this.sunbathAnchorX = null; + this.sunbathAnchorY = null; this.goIdle("\u65e5\u5149\u6d74\u3092\u7d42\u3048\u305f"); } }, @@ -2922,7 +3005,7 @@ function protectSleepSession(tarinai, world, needs) { const beforeBirthTimer = this.birthRitualTimer; this.birthRitualTimer = Math.max(0, this.birthRitualTimer - dt); if (beforeBirthTimer > 0 && this.birthRitualTimer <= 0.04 && this.birthRitualLeader) { - const partner = this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead); + const partner = this.world.liveTarinaiById?.(this.birthPartnerId); if (partner) this.world.finishBirthRitual(this, partner); this.birthRitualLeader = false; } @@ -2952,7 +3035,7 @@ function protectSleepSession(tarinai, world, needs) { this.fightCooldown = Math.max(0, this.fightCooldown - dt); this.defeatedTimer = Math.max(0, this.defeatedTimer - dt); if (wasFighting && this.fightTimer <= 0.04 && this.defeatedById) { - const winner = this.world.tarinai.find(o => o.id === this.defeatedById && !o.dead) || null; + const winner = this.world.liveTarinaiById?.(this.defeatedById) || null; this.defeatedTimer = Math.max(this.defeatedTimer, rand(3.4, 5.2)); this.fearTimer = Math.max(this.fearTimer, 1.0 * this.personalityProfile().fear); this.hurtTimer = Math.max(this.hurtTimer, 1.8); @@ -3025,11 +3108,8 @@ function protectSleepSession(tarinai, world, needs) { this.intentLockTimer = Math.max(0, (this.intentLockTimer || 0) - dt); this.aiTimer += dt; const urgentAi = this.fightTimer > 0.04 || this.intimidateTimer > 0.04 || this.intimidatedTimer > 0.04 || this.birthRitualTimer > 0.04 || this.defeatedTimer > 0.04 || this.eatTimer > 0.02 || this.hurtTimer > 0.02; - const quality = this.world.performanceQuality ? this.world.performanceQuality() : {}; - const pressure = clamp(Number(quality.pressure || 0) || 0, 0, 1); - const intervalScale = Number(quality.aiIntervalScale || 1) || 1; - const phaseOffset = stableUnit(this.familyKey || this.id, "ai-phase") * 0.12 * intervalScale; - const aiInterval = urgentAi ? (0.065 + pressure * 0.045) : ((0.18 + stableUnit(this.familyKey || this.id, "ai-interval") * 0.09) * intervalScale + phaseOffset); + const aiJitter = stableUnit(this.familyKey || this.id, "ai-interval") * 0.35; + const aiInterval = urgentAi ? 0.35 : (1.0 + aiJitter); if (this.aiTimer >= aiInterval && this.world.spendScheduledWork?.("ai", urgentAi) !== false) { const aiDt = Math.min(this.aiTimer, 0.48); this.aiTimer = 0; @@ -3047,7 +3127,7 @@ function protectSleepSession(tarinai, world, needs) { this.updateFacing(); } this.envTimer += dt; - const envInterval = (0.48 + stableUnit(this.familyKey || this.id, "env-phase") * 0.18) * (Number(quality.envIntervalScale || 1) || 1); + const envInterval = 3.0 + stableUnit(this.familyKey || this.id, "env-phase") * 1.4; if (this.envTimer >= envInterval && this.world.spendScheduledWork?.("env", urgentAi || this.world.selected === this) !== false) { const envDt = Math.min(this.envTimer, 1.2); this.envTimer = 0; @@ -3099,10 +3179,11 @@ function protectSleepSession(tarinai, world, needs) { } }, - bubble(text, cooldown = 2.4, color = "rgba(42,36,29,0.78)") { - if (this.world.time < this.nextBubbleAt) return; + bubble(text, cooldown = 2.4, color = "rgba(42,36,29,0.78)", options = {}) { + if (!options.force && this.world.time < this.nextBubbleAt) return false; this.nextBubbleAt = this.world.time + cooldown; this.world.spawnBubble(this.x, this.y - this.radius * 1.35, text, color); + return true; }, mouthPosition(target = null) { @@ -3245,14 +3326,14 @@ function protectSleepSession(tarinai, world, needs) { } if (this.birthRitualTimer > 0.04) { - const partner = this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead) || null; + const partner = this.world.liveTarinaiById?.(this.birthPartnerId) || null; this.setActionState?.("birth_ritual", { target: partner, reason: "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b" }); synchronizeActionTextFromState(this); return; } if (this.intimidateTimer > 0.04) { - const target = this.world.tarinai.find(o => o.id === this.intimidateTargetId && !o.dead) || null; + const target = this.world.liveTarinaiById?.(this.intimidateTargetId) || null; this.setActionState?.("intimidate", { target, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b" }); if (typeof setLiveActionText === "function") setLiveActionText(this, { need: "social", subNeed: "conflict", actionId: "intimidate_enemy", actionLabel: "\u5a01\u5687\u3057\u3066\u3044\u308b", reasonText: target?.name ? `${target.name}\u3092\u5a01\u5687\u3057\u3066\u3044\u308b` : "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b", target, phase: "perform", source: "behavior" }); this.bubble("!", 1.2); @@ -3260,11 +3341,11 @@ function protectSleepSession(tarinai, world, needs) { } if (this.fightTimer > 0.04) { - this.fightTargetIds = (this.fightTargetIds || []).filter(id => this.world.tarinai.some(t => t.id === id && !t.dead)); + this.fightTargetIds = (this.fightTargetIds || []).filter(id => Boolean(this.world.liveTarinaiById?.(id))); this.fightTargetId = this.fightTargetIds[0] || this.fightTargetId; let best = null, bestD = Infinity; for (const id of this.fightTargetIds) { - const t = this.world.tarinai.find(o => o.id === id && !o.dead); + const t = this.world.liveTarinaiById?.(id); if (!t) continue; const d = dist(this, t); if (d < bestD) { best = t; bestD = d; } @@ -3277,7 +3358,7 @@ function protectSleepSession(tarinai, world, needs) { } if (this.defeatedTimer > 0.04) { - const opponent = this.world.tarinai.find(o => o.id === this.defeatedById && !o.dead); + const opponent = this.world.liveTarinaiById?.(this.defeatedById); applyNeedShock(this, { safety: 54, health: 16 }, opponent || null); startNeedDrivenEmergencyReaction(this, opponent || null, "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u9003\u3052\u3066\u3044\u308b\u3002"); if (!opponent && this.defeatedTimer < 0.12) this.defeatedById = null; @@ -3327,6 +3408,21 @@ function protectSleepSession(tarinai, world, needs) { return; } + const foodNeedForLeisure = Number(needs?.food || 0) || 0; + const safetyNeedForLeisure = Number(needs?.safety || 0) || 0; + const sleepNeedForLeisure = Number(needs?.sleep || 0) || 0; + const sunbathActionForLeisure = actionById("sunbath"); + if (sunbathActionForLeisure + && this.canStartSunbath?.({ leisure: true }) + && foodNeedForLeisure < 58 + && safetyNeedForLeisure < 50 + && sleepNeedForLeisure < 68 + && Math.random() < Math.max(0.010, dt * 0.055)) { + const choice = { need: "fulfill", tiedNeeds: ["fulfill"], max: Number(needs?.fulfill || 0) || 0, priority: actionPriority(sunbathActionForLeisure) }; + startNeedAction(this, this.world, choice, sunbathActionForLeisure, "余裕があるので、日光浴している。", { leisure: true, priority: actionPriority(sunbathActionForLeisure), lockSeconds: 6.0 }); + return; + } + const picked = choosePriorityNeedAction(needs, this, this.world); if (!picked?.action) { const sunbathAction = actionById("sunbath"); @@ -3350,27 +3446,28 @@ function protectSleepSession(tarinai, world, needs) { }, makePoop(force = 1) { - this.digest += force; - if (this.digest < 12.4) return; + const add = Math.max(0, Number(force) || 0); + if (add <= 0) return false; + const threshold = NORMAL_POOP_MEAL_THRESHOLD || 3; + this.digest = Math.max(0, Number(this.digest) || 0) + add; + if (this.digest + 1e-6 < threshold) return false; + this.digest = Math.max(0, this.digest - threshold); if (this.hasLodgedPinEffect?.("blocksZunchi")) { - this.digest = rand(0.4, 1.6); this.oshiriByoZunchiStock = Math.min(18, Math.max(0, this.oshiriByoZunchiStock || 0) + 1); this.thought = (this.oshiriByoZunchiStock || 0) >= 6 ? "\u304a\u3057\u308a\u75c5\u3067\u305a\u3093\u3061\u304c\u6e9c\u307e\u3063\u3066\u3064\u3089\u3044" : "\u304a\u3057\u308a\u75c5\u3067\u305a\u3093\u3061\u3092\u6211\u6162\u3057\u3066\u3044\u308b"; if ((this.oshiriByoZunchiStock || 0) >= 6) applyNeedShock(this, { health: 8 }); - return; + return false; } - this.digest = rand(0.4, 1.6); - this.poopCount += 1; + this.poopCount = (this.poopCount || 0) + 1; const side = this.facingDir(); const backX = this.x - side * this.radius * rand(0.72, 0.96); const backY = this.y + this.radius * rand(0.25, 0.48); this.world.spawnZunchi(backX, backY, this); const now = this.world?.time || 0; - if (now >= (this.nextPoopBubbleAt || -999)) { - this.nextPoopBubbleAt = now + 2.8; - this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)"); - } + this.nextPoopBubbleAt = now + 2.8; + this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)", { force: true }); if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] }); + return true; }, interactWithItems(dt) { @@ -3385,8 +3482,7 @@ function protectSleepSession(tarinai, world, needs) { const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && mealReady && wantsFood && this.hunger > 18; const canSweetBite = !this.isZunchiSlave && this.eatCooldown <= 0 && mealReady && (wantsFood || wantsMedicine) && this.hunger > 7; const canZunchiBite = this.eatCooldown <= 0 && (this.isZunchiSlave ? this.hunger > 16 : (this.hunger > 108 || foodNeed > 98)) && (this.isZunchiSlave || !hasBetterFoodThanZunchiNearby(this, this.world, 240)); - const quality = this.world?.performanceQuality?.() || {}; - const scanLimit = Math.max(3, Number(quality.itemScanLimit || 28) || 28); + const scanLimit = 16; const nearbyItems = this.world.nearbyItems(this.x, this.y, this.radius + 46); const foodCandidates = []; if (this.target && !this.target.dead && (this.target.type === "duplicator" || roleMatches(this.target, "food") || roleMatches(this.target, "medicine"))) foodCandidates.push(this.target); @@ -3432,7 +3528,7 @@ function protectSleepSession(tarinai, world, needs) { } this.lastMealColor = foodType === "sweet" ? "#78b957" : "#f1dfb7"; const fromDuplicator = isDuplicatorFoodSource(it); - this.makePoop(eating * (fromDuplicator ? duplicatorPoopMultiplier(foodType) : (foodType === "sweet" ? 0.18 : 0.22))); + this.makePoop(normalPoopUnitsFor(foodType || it.type, eating)); if (fromDuplicator) this.mealCooldownUntil = Math.max(this.mealCooldownUntil || 0, this.world.time + 1.2); if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; } if (this.world.time - this.lastLog > 9 && Math.random() < 0.014) { @@ -3479,7 +3575,7 @@ function protectSleepSession(tarinai, world, needs) { this.world.spawnEatEffect(mouth.x, mouth.y, foodType === "love_mochi" ? "#ff7bab" : (foodType === "fight_mochi" ? "#e07c43" : (foodType === "sleep_drug" ? "#b89cff" : (this.lastMealColor || "#41aa66")))); } const fromDuplicator = isDuplicatorFoodSource(it); - this.makePoop(eating * (fromDuplicator ? duplicatorPoopMultiplier(foodType) : 0.20)); + this.makePoop(normalPoopUnitsFor(foodType || it.type, eating)); if (fromDuplicator) this.mealCooldownUntil = Math.max(this.mealCooldownUntil || 0, this.world.time + 1.2); if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; } if (this.world.time - this.lastLog > 9 && Math.random() < 0.02) { @@ -3510,7 +3606,7 @@ function protectSleepSession(tarinai, world, needs) { this.world.spawnEatEffect(mouth.x, mouth.y, "#5b9d39"); } if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.55; } - this.makePoop(bite * 0.10); + this.makePoop(normalPoopUnitsFor("grass", bite)); if (this.type === "jito") this.affection += dt * 0.25; break; } @@ -3531,7 +3627,7 @@ function protectSleepSession(tarinai, world, needs) { const mouth = this.mouthPosition(it); this.world.spawnEatEffect(mouth.x, mouth.y, "#4a3f36"); this.lastMealColor = "#4a3f36"; - this.makePoop(bite * 0.08); + this.makePoop(normalPoopUnitsFor("ant_corpse", bite)); if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.62; } break; } diff --git a/js/tarinai_render.js b/js/tarinai_render.js index 8d9dafc..64179fe 100644 --- a/js/tarinai_render.js +++ b/js/tarinai_render.js @@ -4,14 +4,145 @@ const Tarinai = global.Tarinai; if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_render.js"); Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ + drawSunbathPose(ctx, t, lighting = null) { + if (this.dead) return true; + const active = this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04; + if (!active) return false; + + this.preloadSunbathSprites?.(); + const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"]; + const candidates = (Array.isArray(ids) && ids.length ? ids : ["sunbath_1", "sunbath_2", "sunbath_3"]).filter(id => String(id || "").startsWith("sunbath")); + const ready = (id) => typeof isImageReady === "function" + ? isImageReady(id) + : !!(typeof images !== "undefined" && images?.get?.(id)?.complete && (images?.get?.(id)?.naturalWidth || images?.get?.(id)?.width)); + const imageFor = (id) => { + if (!id) return null; + if (typeof getRenderableImage === "function") return getRenderableImage(id, ""); + return (typeof images !== "undefined" ? images.get?.(id) : globalThis.TarinaiAssets?.images?.get?.(id)) || null; + }; + + let requested = this.sunbathSpriteId ? this.sunbathSpriteId() : this.normalizeSunbathSpriteId?.(this.sunbathSpriteVariant); + if (!requested || !candidates.includes(requested)) requested = candidates[0] || "sunbath_1"; + + const locked = this._activeSunbathSpriteId && candidates.includes(this._activeSunbathSpriteId) && ready(this._activeSunbathSpriteId) + ? this._activeSunbathSpriteId + : ""; + const ordered = [locked, requested, this._lastRenderableSunbathSpriteId, ...candidates].filter(Boolean); + let renderSprite = ordered.find(id => candidates.includes(id) && ready(id)); + let img = renderSprite ? imageFor(renderSprite) : null; + + if (img && ready(renderSprite)) { + this._activeSunbathSpriteId = renderSprite; + this._lastRenderableSunbathSpriteId = renderSprite; + this.sunbathSpriteVariant = renderSprite; + this.visibleSpriteId = renderSprite; + } else { + const fallbackId = this._lastNonSunbathSpriteId || this.goodMode || this.type || "smile"; + renderSprite = fallbackId; + img = typeof getRenderableImage === "function" + ? getRenderableImage(fallbackId, "smile") + : (typeof images !== "undefined" ? (images.get?.(fallbackId) || images.get?.("smile")) : null); + if (!img) { + renderSprite = "smile"; + img = typeof getRenderableImage === "function" + ? getRenderableImage("smile", "smile") + : (typeof images !== "undefined" ? images.get?.("smile") : null); + } + } + + const worldW = Math.max(1, Number(this.world?.w || 1000) || 1000); + const worldH = Math.max(1, Number(this.world?.h || 720) || 720); + const pad = Math.max(28, CONFIG.worldPadding || 30); + const lastX = Number.isFinite(this._lastSunbathDrawX) ? this._lastSunbathDrawX : (Number.isFinite(this._lastValidX) ? this._lastValidX : NaN); + const lastY = Number.isFinite(this._lastSunbathDrawY) ? this._lastSunbathDrawY : (Number.isFinite(this._lastValidY) ? this._lastValidY : NaN); + if (!Number.isFinite(this.sunbathAnchorX)) this.sunbathAnchorX = Number.isFinite(lastX) ? lastX : (Number.isFinite(this.x) ? this.x : pad); + if (!Number.isFinite(this.sunbathAnchorY)) this.sunbathAnchorY = Number.isFinite(lastY) ? lastY : (Number.isFinite(this.y) ? this.y : pad); + let drawX = this.sunbathAnchorX; + let drawY = this.sunbathAnchorY; + if (!Number.isFinite(drawX)) drawX = Number.isFinite(lastX) ? lastX : pad; + if (!Number.isFinite(drawY)) drawY = Number.isFinite(lastY) ? lastY : pad; + drawX = clamp(drawX, pad, Math.max(pad, worldW - pad)); + drawY = clamp(drawY, pad, Math.max(pad, worldH - pad)); + this.sunbathAnchorX = drawX; + this.sunbathAnchorY = drawY; + this._lastSunbathDrawX = drawX; + this._lastSunbathDrawY = drawY; + this._lastValidX = drawX; + this._lastValidY = drawY; + if (!Number.isFinite(this.x) || Math.abs(this.x - drawX) > 1.5) this.x = drawX; + if (!Number.isFinite(this.y) || Math.abs(this.y - drawY) > 1.5) this.y = drawY; + + const metrics = getImageMetrics(renderSprite) || getImageMetrics(img) || getImageMetrics("smile") || {}; + const w = metrics?.w || img?.naturalWidth || img?.width || 512; + const h = metrics?.h || img?.naturalHeight || img?.height || 512; + const visualScale = this.effectiveScale ? this.effectiveScale() : this.scale; + const drawW = w * visualScale * 0.30; + const drawH = h * visualScale * 0.30; + const breathing = Math.sin(t * 1.25 + this.age * 0.18); + const bob = breathing * 0.55; + const faceRight = this.facingDir ? this.facingDir() > 0 : (this.facing || 1) > 0; + const nestHideAlpha = this.nestFade ? clamp(1 - this.nestFade, 0, 1) : 1; + if (nestHideAlpha <= 0.01) return true; + + const lightState = lighting || getLightingState(this.world); + const showDecor = true || this.world.selected === this; + if (showDecor) { + const shadow = projectedShadowParams(lightState, 0.95 + visualScale * 0.8); + drawProjectedShadow(ctx, drawX, drawY + drawH * 0.30, drawW * 0.58, drawH * 0.13, { ...shadow, alpha: shadow.alpha * 0.90 * nestHideAlpha }); + } + + ctx.save(); + ctx.globalAlpha = nestHideAlpha; + ctx.translate(drawX, drawY + bob); + ctx.rotate((faceRight ? 1 : -1) * Math.PI / 28); + if (faceRight) ctx.scale(-1, 1); + if (img) { + ctx.drawImage(img, -drawW / 2, -drawH / 2, drawW, drawH); + } else { + ctx.fillStyle = "#f4ead6"; + ctx.strokeStyle = "rgba(92,71,49,0.48)"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.ellipse(0, 0, drawW * 0.42, drawH * 0.36, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + } + ctx.restore(); + + if (showDecor) { + ctx.save(); + ctx.globalAlpha = 0.18 * nestHideAlpha; + ctx.fillStyle = lightState.goldenStrength > 0.25 ? "rgba(255, 223, 137, 0.95)" : "rgba(255, 244, 172, 0.82)"; + ctx.beginPath(); + ctx.arc(drawX - drawW * 0.28, drawY - drawH * 0.35, Math.max(3, drawW * 0.06), 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } + return true; + }, + draw(ctx, t, lighting = null) { if (this.dead) return; + const sunbathActive = this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04; + if (!sunbathActive && Number.isFinite(this.x) && Number.isFinite(this.y)) { + this._lastValidX = this.x; + this._lastValidY = this.y; + } const lightState = lighting || getLightingState(this.world); - const quality = this.world.performanceQuality?.() || {}; - const effectDensity = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1); - const showDecor = effectDensity > 0.45 || this.world.selected === this; - if (this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04) this.preloadSunbathSprites?.(); - const sprite = this.spriteId(); + if (sunbathActive && this.drawSunbathPose?.(ctx, t, lightState)) return; + if (!sunbathActive && String(this.visibleSpriteId || "").startsWith("sunbath")) { + this.visibleSpriteId = ""; + this.spriteLockUntil = 0; + } + const showDecor = true || this.world.selected === this; + if (sunbathActive) this.preloadSunbathSprites?.(); + let sprite = this.spriteId(); + if (!sunbathActive && String(sprite || "").startsWith("sunbath")) { + this.visibleSpriteId = ""; + this.spriteLockUntil = 0; + sprite = this.rawSpriteId ? this.rawSpriteId() : (this.goodMode || this.type || "smile"); + } + if (!String(sprite || "").startsWith("sunbath")) this._lastNonSunbathSpriteId = sprite; const sunbathSprite = String(sprite || "").startsWith("sunbath"); const sunbathCandidates = sunbathSprite && this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : []; if (sunbathSprite) { @@ -20,18 +151,28 @@ else if (globalThis.TarinaiAssets?.ensureImage) globalThis.TarinaiAssets.ensureImage(id); } } - const spriteFallback = sunbathSprite ? "" : "smile"; + const spriteFallback = "smile"; let renderSprite = sprite; let img = typeof getRenderableImage === "function" ? getRenderableImage(sprite, spriteFallback) : (images.get(sprite) || images.get(spriteFallback) || images.get(this.type)); if (sunbathSprite) { const ready = (id) => typeof isImageReady === "function" ? isImageReady(id) : !!(images?.get?.(id)?.complete || images?.get?.(id)?.naturalWidth || images?.get?.(id)?.width); - if (!ready(sprite)) { - const fallbackSunbath = sunbathCandidates.find(id => ready(id)); + const imageFor = (id) => (typeof images !== "undefined" ? images.get?.(id) : globalThis.TarinaiAssets?.images?.get?.(id)) || null; + if (ready(sprite)) { + this._lastRenderableSunbathSpriteId = sprite; + } else { + const orderedFallbacks = [this._lastRenderableSunbathSpriteId, ...sunbathCandidates].filter(Boolean); + const fallbackSunbath = orderedFallbacks.find(id => ready(id)); if (fallbackSunbath) { renderSprite = fallbackSunbath; - img = images.get(fallbackSunbath); + img = imageFor(fallbackSunbath); + this._lastRenderableSunbathSpriteId = fallbackSunbath; + this.sunbathSpriteVariant = fallbackSunbath; } } + if (!img && this._lastRenderableSunbathSpriteId && ready(this._lastRenderableSunbathSpriteId)) { + renderSprite = this._lastRenderableSunbathSpriteId; + img = imageFor(this._lastRenderableSunbathSpriteId); + } if (!img) img = typeof getRenderableImage === "function" ? getRenderableImage("smile", "smile") : (images.get("smile") || images.get(this.type)); } const metrics = getImageMetrics(renderSprite) || getImageMetrics(sprite) || getImageMetrics(this.type) || getImageMetrics("smile"); @@ -131,7 +272,7 @@ ctx.restore(); } if (this.birthRitualTimer > 0.04 && this.birthRitualLeader) { - const partner = this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead); + const partner = this.world.liveTarinaiById?.(this.birthPartnerId); const hx = partner ? (this.x + partner.x) / 2 : this.x; const hy = partner ? Math.min(this.y, partner.y) - Math.max(drawH, partner.radius * 2.6) * 0.50 : this.y - drawH * 0.60; const pulse = 1 + Math.sin(t * 5.5 + this.age) * 0.10; @@ -287,19 +428,19 @@ const cacheW = Math.max(1, Math.round((dw * renderScale) / 8) * 8); const cacheH = Math.max(1, Math.round((dh * renderScale) / 8) * 8); if (img) { - let spriteCacheKey = sprite; + let spriteCacheKey = renderSprite || sprite; try { - if (typeof images !== "undefined" && images.get?.(sprite) !== img) { + if (typeof images !== "undefined" && images.get?.(renderSprite || sprite) !== img) { let fallbackId = "fallback"; for (const [imageId, imageObj] of images) { if (imageObj === img) { fallbackId = imageId; break; } } - spriteCacheKey = `${sprite}:fallback:${fallbackId}`; + spriteCacheKey = `${renderSprite || sprite}:fallback:${fallbackId}`; } } catch (err) { - spriteCacheKey = sprite; + spriteCacheKey = renderSprite || sprite; } - const spriteCanvas = sunbathSprite ? img : (typeof getCachedSpriteCanvas === "function" ? getCachedSpriteCanvas(spriteCacheKey, img, cacheW, cacheH) : img); + const spriteCanvas = typeof getCachedSpriteCanvas === "function" ? getCachedSpriteCanvas(spriteCacheKey, img, cacheW, cacheH) : img; ctx.drawImage(spriteCanvas, -dw / 2, -dh / 2, dw, dh); } else { ctx.save(); @@ -398,87 +539,6 @@ } }, - drawSunbathSpriteDirect(ctx, t, lightState, showDecor = true) { - return false; - const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"]; - const candidates = (Array.isArray(ids) && ids.length ? ids : ["sunbath_1", "sunbath_2", "sunbath_3"]).filter(id => String(id || "").startsWith("sunbath")); - const now = Number(this.world?.time ?? t ?? 0) || 0; - const frameKey = Math.floor(now / 0.5); - if (this._sunbathRenderFrameKey !== frameKey || !candidates.includes(this._sunbathRenderSpriteId)) { - const prev = this._sunbathRenderSpriteId || this.sunbathSpriteVariant || ""; - const choices = candidates.length > 1 ? candidates.filter(id => id !== prev) : candidates; - this._sunbathRenderSpriteId = choices[Math.floor(Math.random() * choices.length)] || candidates[0] || "sunbath_1"; - this._sunbathRenderFrameKey = frameKey; - } - const sprite = this._sunbathRenderSpriteId || this.sunbathSpriteVariant || candidates[0] || "sunbath_1"; - this.sunbathSpriteVariant = sprite; - this.visibleSpriteId = sprite; - this.spriteLockUntil = now; - for (const id of candidates) { - if (typeof ensureImage === "function") ensureImage(id); - else if (globalThis.TarinaiAssets?.ensureImage) globalThis.TarinaiAssets.ensureImage(id); - } - - let img = null; - const ordered = [sprite, this.sunbathSpriteVariant, ...candidates, "smile"]; - const seen = new Set(); - for (const id of ordered) { - if (!id || seen.has(id)) continue; - seen.add(id); - const next = typeof getRenderableImage === "function" - ? getRenderableImage(id, id === "smile" ? "smile" : "smile") - : (typeof images !== "undefined" ? (images.get(id) || images.get("smile")) : null); - const ready = next && (next.complete || next.naturalWidth || next.width); - if (ready) { img = next; break; } - } - - const metrics = (typeof getImageMetrics === "function" ? (getImageMetrics(sprite) || getImageMetrics(img) || getImageMetrics("smile")) : null) || {}; - const w = metrics.w || img?.naturalWidth || img?.width || 512; - const h = metrics.h || img?.naturalHeight || img?.height || 468; - const visualScale = this.effectiveScale ? this.effectiveScale() : this.scale; - const breathing = Math.sin(t * 1.4 + this.age * 0.18); - const bob = breathing * 0.55; - const drawW = w * visualScale * 0.30; - const drawH = h * visualScale * 0.30; - const squish = 1 + breathing * 0.006; - const dw = drawW * squish; - const dh = drawH / squish; - const faceRight = this.facingDir ? this.facingDir() > 0 : (this.facing || 1) > 0; - const nestHideAlpha = this.nestFade ? clamp(1 - this.nestFade, 0, 1) : 1; - if (nestHideAlpha <= 0.01) return true; - - const shadow = projectedShadowParams(lightState, 0.95 + visualScale * 0.8); - const shadowContactY = this.y + bob + dh * 0.30; - if (showDecor && img) { - drawImageProjectedShadow(ctx, img, this.x, shadowContactY, dw, dh, shadow, { - faceRight, - alpha: shadow.alpha * 0.92 * nestHideAlpha, - }); - } else if (showDecor) { - drawProjectedShadow(ctx, this.x, shadowContactY, drawW * 0.52, drawH * 0.13, { ...shadow, alpha: shadow.alpha * 0.82 * nestHideAlpha }); - } - if (showDecor) drawCreatureGlow(ctx, this, drawW, drawH, lightState); - - ctx.save(); - ctx.translate(this.x, this.y + bob); - ctx.rotate((faceRight ? 1 : -1) * (Math.PI / 18)); - if (faceRight) ctx.scale(-1, 1); - ctx.globalAlpha = nestHideAlpha; - if (img) { - ctx.drawImage(img, -dw / 2, -dh / 2, dw, dh); - } else { - ctx.fillStyle = "#f4ead6"; - ctx.strokeStyle = "rgba(92,71,49,0.48)"; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.ellipse(0, 0, dw * 0.42, dh * 0.36, 0, 0, Math.PI * 2); - ctx.fill(); - ctx.stroke(); - } - ctx.restore(); - return true; - }, - contains(x, y) { if (this.insideNestBoxId || (this.nestFade || 0) >= 0.92) return false; const hit = this.world.screenSizeToWorld ? this.world.screenSizeToWorld(this.radius * 1.9) : this.radius * 1.9; diff --git a/js/tarinai_social_move_life.js b/js/tarinai_social_move_life.js index 557e1d3..76d461a 100644 --- a/js/tarinai_social_move_life.js +++ b/js/tarinai_social_move_life.js @@ -6,8 +6,7 @@ Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ interactWithOthers(dt) { let closeCount = 0; - const quality = this.world?.performanceQuality?.() || {}; - const scanLimit = Math.max(1, Number(quality.socialScanLimit || 14) || 14); + const scanLimit = 8; let scanned = 0; for (const o of this.world.nearbyTarinai(this.x, this.y, 78)) { if (o === this || o.dead) continue; @@ -191,13 +190,23 @@ if (this.state === "sunbath") { this.target = null; - this.vx *= Math.pow(0.90, dt * 60); - this.vy *= Math.pow(0.90, dt * 60); + this.vx = 0; + this.vy = 0; const pad = Math.max(28, CONFIG.worldPadding || 30); const safeW = Math.max(pad * 2 + 1, Number(this.world?.w || 1000) || 1000); const safeH = Math.max(pad * 2 + 1, Number(this.world?.h || 720) || 720); - this.x = clamp((Number.isFinite(this.x) ? this.x : safeW * 0.5) + Math.sin((this.world?.time || 0) * 1.3 + this.seed) * 0.03, pad, safeW - pad); - this.y = clamp((Number.isFinite(this.y) ? this.y : safeH * 0.5) + Math.cos((this.world?.time || 0) * 1.1 + this.seed) * 0.02, pad, safeH - pad); + const fallbackX = Number.isFinite(this._lastSunbathDrawX) ? this._lastSunbathDrawX : (Number.isFinite(this._lastValidX) ? this._lastValidX : (Number.isFinite(this.x) ? this.x : pad)); + const fallbackY = Number.isFinite(this._lastSunbathDrawY) ? this._lastSunbathDrawY : (Number.isFinite(this._lastValidY) ? this._lastValidY : (Number.isFinite(this.y) ? this.y : pad)); + if (!Number.isFinite(this.sunbathAnchorX)) this.sunbathAnchorX = fallbackX; + if (!Number.isFinite(this.sunbathAnchorY)) this.sunbathAnchorY = fallbackY; + this.sunbathAnchorX = clamp(this.sunbathAnchorX, pad, safeW - pad); + this.sunbathAnchorY = clamp(this.sunbathAnchorY, pad, safeH - pad); + this.x = this.sunbathAnchorX; + this.y = this.sunbathAnchorY; + this._lastSunbathDrawX = this.x; + this._lastSunbathDrawY = this.y; + this._lastValidX = this.x; + this._lastValidY = this.y; return; } if (this.state === "sleep") { @@ -234,7 +243,7 @@ } if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") { - const partner = this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead); + const partner = this.world.liveTarinaiById?.(this.birthPartnerId); if (partner) { const midX = (this.x + partner.x) / 2; const midY = (this.y + partner.y) / 2; @@ -418,7 +427,11 @@ this.world.addItem?.(new Item("splat", this.x, this.y), "tarinai-death-splat") || this.world.items.push(new Item("splat", this.x, this.y)); if (Math.random() < 0.55) { const spot = this.world.findGrassPlantingSpot(this.x, this.y, { allowOriginal: false, minRadius: 26, maxRadius: 76 }); - if (spot) this.world.addItem?.(new Item("grass", spot.x, spot.y), "tarinai-death-grass") || this.world.items.push(new Item("grass", spot.x, spot.y)); + if (spot) { + const deathGrass = new Item("grass", spot.x, spot.y); + if (this.world.addItem) this.world.addItem(deathGrass, "tarinai-death-grass"); + else this.world.items.push(deathGrass); + } } this.world.markDead(this, finalReason); this.releasedLiveToken = this.liveToken || 0; diff --git a/js/ui_layout_dialogs.js b/js/ui_layout_dialogs.js index 113acd6..bf1eb33 100644 --- a/js/ui_layout_dialogs.js +++ b/js/ui_layout_dialogs.js @@ -2,10 +2,7 @@ function resizeCanvas() { const rect = canvas.getBoundingClientRect(); - const quality = world?.performanceQuality?.() || {}; - const perf = Number(quality.level ?? (world?.performanceLevel ? world.performanceLevel() : 0)) || 0; - const maxDpr = Number(quality.maxDpr || 0) || (perf >= 2 ? 1 : (perf === 1 ? 1.25 : 2)); - const dpr = Math.max(0.6, Math.min(window.devicePixelRatio || 1, maxDpr)); + const dpr = Math.max(0.6, Math.min(window.devicePixelRatio || 1, 2)); const nextW = Math.max(1, Math.floor(rect.width * dpr)); const nextH = Math.max(1, Math.floor(rect.height * dpr)); if (canvas.width === nextW && canvas.height === nextH && uiCache.canvasDpr === dpr) return; diff --git a/js/version.js b/js/version.js index 8bc7124..f403220 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "15.24.53"; - const APP_BUILD = "tool-ui-icon-scale-collapse-fix"; + const APP_VERSION = "16.00.01"; + const APP_BUILD = "lag-pass-static-schedule"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; diff --git a/js/weather_system.js b/js/weather_system.js index ea67d8c..360421c 100644 --- a/js/weather_system.js +++ b/js/weather_system.js @@ -20,7 +20,6 @@ shouldDropRainWater(worldRef, dt = 0) { return Boolean( worldRef?.weather === "light_rain" && - (worldRef.itemCounts?.water || 0) < 24 && Math.random() < Math.max(0, Number(dt) || 0) * 0.12 ); }, diff --git a/js/world_ants_system.js b/js/world_ants_system.js index 1a5a4f3..76f5a5f 100644 --- a/js/world_ants_system.js +++ b/js/world_ants_system.js @@ -6,35 +6,40 @@ Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ updateAnts(dt) { if (!this.ants) this.ants = []; - const nests = this.itemsOfType?.("ant_nest") || (this.items || []).filter(it => it && !it.dead && it.type === "ant_nest"); - for (const nest of nests) { - if (typeof ensureAntNestWorkerPool === "function") ensureAntNestWorkerPool(nest); - if (!Number.isFinite(nest.antCount)) nest.antCount = ANT_NEST_START_COUNT || 6; - if (typeof syncAntNestCount === "function") syncAntNestCount(this, nest); - nest.antSpawnTimer = Math.max(0, (nest.antSpawnTimer || 0) - dt); - const outside = this.ants.filter(a => a && !a.dead && a.kind === "worker" && a.homeId === nest.id).length; - const available = Array.isArray(nest.antWorkers) ? nest.antWorkers.length : Math.max(0, (nest.antCount || 0) - outside); - const maxOutside = Math.min(ANT_NEST_MAX_OUTSIDE || 3, nest.antCount || 0); - if (available > 0 && outside < maxOutside && (nest.antSpawnTimer || 0) <= 0 && (this.tarinai || []).some(t => t && !t.dead && !t.insideNestBoxId)) { - const hp = Array.isArray(nest.antWorkers) && nest.antWorkers.length ? clamp(nest.antWorkers.pop(), 0, ANT_WORKER_HP || 32) : (ANT_WORKER_HP || 32); - const a = new AntActor(this, { - kind: "worker", - homeId: nest.id, - x: nest.x + rand(-nest.r * 0.18, nest.r * 0.18), - y: nest.y - nest.r * 1.00 + rand(-3, 3), - hp, - maxHp: ANT_WORKER_HP || 32, - }); - this.ants.push(a); - this.emit?.("ant:spawned", { ant: a, nest }); + this.antNestUpdateAccum = (this.antNestUpdateAccum || 0) + dt; + if (this.antNestUpdateAccum >= 3.0) { + const nestDt = this.antNestUpdateAccum; + this.antNestUpdateAccum = 0; + const nests = this.itemsOfType?.("ant_nest") || (this.items || []).filter(it => it && !it.dead && it.type === "ant_nest"); + for (const nest of nests) { + if (typeof ensureAntNestWorkerPool === "function") ensureAntNestWorkerPool(nest); + if (!Number.isFinite(nest.antCount)) nest.antCount = ANT_NEST_START_COUNT || 6; if (typeof syncAntNestCount === "function") syncAntNestCount(this, nest); - nest.antSpawnTimer = rand(1.6, 3.8) + outside * 0.45; - this.drawListDirty = true; + nest.antSpawnTimer = Math.max(0, (nest.antSpawnTimer || 0) - nestDt); + let outside = 0; + for (const a of this.ants) if (a && !a.dead && a.kind === "worker" && a.homeId === nest.id) outside += 1; + const available = Array.isArray(nest.antWorkers) ? nest.antWorkers.length : Math.max(0, (nest.antCount || 0) - outside); + const maxOutside = Math.min(ANT_NEST_MAX_OUTSIDE || 3, nest.antCount || 0); + if (available > 0 && outside < maxOutside && (nest.antSpawnTimer || 0) <= 0 && (this.tarinai || []).some(t => t && !t.dead && !t.insideNestBoxId)) { + const hp = Array.isArray(nest.antWorkers) && nest.antWorkers.length ? clamp(nest.antWorkers.pop(), 0, ANT_WORKER_HP || 32) : (ANT_WORKER_HP || 32); + const a = new AntActor(this, { + kind: "worker", + homeId: nest.id, + x: nest.x + rand(-nest.r * 0.18, nest.r * 0.18), + y: nest.y - nest.r * 1.00 + rand(-3, 3), + hp, + maxHp: ANT_WORKER_HP || 32, + }); + this.ants.push(a); + this.emit?.("ant:spawned", { ant: a, nest }); + if (typeof syncAntNestCount === "function") syncAntNestCount(this, nest); + nest.antSpawnTimer = rand(2.5, 5.5) + outside * 0.65; + this.drawListDirty = true; + } } } const aliveAntCount = (this.ants || []).reduce((n, a) => n + (a && !a.dead ? 1 : 0), 0); - const q = this.performanceQuality ? this.performanceQuality() : null; - const stride = Math.max(1, Math.min(3, q?.antStride || (aliveAntCount >= 70 ? 3 : aliveAntCount >= 28 ? 2 : 1))); + const stride = aliveAntCount >= 28 ? 3 : 2; this.antUpdatePhase = ((this.antUpdatePhase || 0) + 1) % stride; for (let i = 0; i < this.ants.length; i++) { const a = this.ants[i]; diff --git a/js/world_combat_effects.js b/js/world_combat_effects.js index 63e3d1c..05d2e35 100644 --- a/js/world_combat_effects.js +++ b/js/world_combat_effects.js @@ -288,15 +288,6 @@ }, spawnZunchi(x, y, source = null) { - const zunchiLimit = CONFIG.zunchiLimit ?? 56; - if ((this.itemCounts.zunchi || 0) >= zunchiLimit) { - let oldest = null; - for (const it of this.items) { - if (it.type !== "zunchi" || it.dead) continue; - if (!oldest || (it.age || 0) > (oldest.age || 0)) oldest = it; - } - if (oldest) oldest.amount = 0; - } const it = new Item("zunchi", clamp(x, 40, this.w - 40), clamp(y, 40, this.h - 40)); it.amount = 260; it.stage = "fresh"; diff --git a/js/world_family_social.js b/js/world_family_social.js index 1a4c3df..ddd5734 100644 --- a/js/world_family_social.js +++ b/js/world_family_social.js @@ -63,7 +63,11 @@ generation: 1, }); } - for (let i = 0; i < grassCount; i++) this.addItem?.(new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70)), "reset-grass") || this.items.push(new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70))); + for (let i = 0; i < grassCount; i++) { + const grass = new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70)); + if (this.addItem) this.addItem(grass, "reset-grass"); + else this.items.push(grass); + } this.addItem?.(new Item("stone", this.w * 0.68, this.h * 0.32), "reset-stone") || this.items.push(new Item("stone", this.w * 0.68, this.h * 0.32)); this.updateItemCounts(); this.updateEffectCounts(); diff --git a/js/world_placement_log.js b/js/world_placement_log.js index 04785bf..2f5d073 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -365,17 +365,10 @@ placeItem(item, dropped = true) { if (!item) return null; - if (item.type === "grass" && (this.itemCounts?.grass || 0) >= (this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 96))) { - showToast("\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u306e\u8349\u306e\u4e0a\u9650\u306b\u9054\u3057\u3066\u3044\u307e\u3059\u3002"); - return null; - } - if (this.isFenceType(item.type)) { - this.compactItems(); - this.updateItemCounts(); - if (this.liveFenceCount() >= this.fenceLimit()) { - showToast("\u67f5\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u5148\u306b\u4e00\u90e8\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002"); - return null; - } + if (item.type === "grass") { + item.grassLimitExempt = true; + item.manualGrass = true; + item.placedByPlayer = true; } if (this.placementBlocked(item)) { showToast(this.isFenceType(item.type) ? "\u305d\u3053\u306b\u306f\u67f5\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3002" : "\u305d\u3053\u306b\u306f\u914d\u7f6e\u3067\u304d\u307e\u305b\u3093\u3002"); @@ -386,7 +379,7 @@ item.dropTimer = item.dropMax; item.dropImpactDone = false; } - this.addItem?.(item, `place:${item.type}`) || this.items.push(item); + this.addItem?.(item, `place:${item.type}`, { ignoreGrassLimit: item.type === "grass", manualGrass: item.type === "grass" }) || this.items.push(item); this.emit("tool:place", { item, type: item.type, dropped }); this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type }); if (dropped) audio.place?.(item.type); diff --git a/js/world_spatial_budget.js b/js/world_spatial_budget.js index fde3219..a634af0 100644 --- a/js/world_spatial_budget.js +++ b/js/world_spatial_budget.js @@ -12,25 +12,17 @@ Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ performanceLevel() { - if (window.TarinaiPerformance) return window.TarinaiPerformance.quality?.(this)?.level || window.TarinaiPerformance.level || 0; - const fps = typeof window !== "undefined" ? Number(window.__tarinaiFps || 0) : 60; - if (!fps || !Number.isFinite(fps)) return 0; - if (fps < 22) return 3; - if (fps < 34) return 2; - if (fps < 48) return 1; return 0; }, performanceQuality() { - return window.TarinaiPerformance?.quality?.(this) || { level: this.performanceLevel(), antStride: 1, effectLimit: CONFIG.effectLimit ?? 96 }; + return { level: 0, pressure: 0, effectDensity: 1, antStride: 2, effectLimit: CONFIG.effectLimit ?? 96, itemScanLimit: 16, socialScanLimit: 8, collisionInterval: 1.8, bedConflictInterval: 3.2, statsInterval: 1.2, maxDpr: 2 }; }, - beginFramePerformanceBudgets(quality = this.performanceQuality?.()) { - const pressure = clamp(Number(quality?.pressure || 0) || 0, 0, 1); - const density = clamp(Number(quality?.effectDensity || (1 - pressure)) || 0, 0.12, 1); + beginFramePerformanceBudgets() { this.workBudget = { - ai: Math.max(4, Math.round(4 + density * 24)), - env: Math.max(3, Math.round(3 + density * 15)), + ai: Math.max(1, Math.ceil(((this.tarinai || []).length || 1) / 2)), + env: Math.max(1, Math.ceil(((this.tarinai || []).length || 1) / 3)), aiUsed: 0, envUsed: 0, }; @@ -106,11 +98,11 @@ }, performance: { liveTarinai, - fps: window.TarinaiPerformance?.fps || window.__tarinaiFps || 0, - pressure: this.performanceQuality?.().pressure || 0, - level: this.performanceLevel?.() || 0, + fps: window.__tarinaiFps || window.TarinaiPerformance?.fps || 0, + pressure: 0, + level: 0, dpr: (typeof uiCache !== "undefined" ? uiCache?.canvasDpr : window.uiCache?.canvasDpr) || 0, - quality: this.performanceQuality?.() || null, + quality: null, }, work: { budget: this.workBudget || null, @@ -121,10 +113,9 @@ grassLoadLevel() { const grass = this.itemCounts?.grass || 0; - const perf = this.performanceLevel(); - if (perf >= 3 || grass >= 180) return 3; - if (perf >= 2 || grass >= 120) return 2; - if (perf >= 1 || grass >= 72) return 1; + if (grass >= 180) return 3; + if (grass >= 120) return 2; + if (grass >= 72) return 1; return 0; }, @@ -132,6 +123,29 @@ return [1.0, 1.35, 1.85, 2.45][this.grassLoadLevel()] || 1.0; }, + grassLimit() { + const limit = Number(CONFIG.grassLimit ?? 360); + return Number.isFinite(limit) && limit >= 0 ? Math.floor(limit) : Infinity; + }, + + isGrassLimitExempt(item) { + return Boolean(item && item.type === "grass" && (item.grassLimitExempt || item.manualGrass || item.placedByPlayer)); + }, + + liveGrassCount() { + let total = 0; + for (const it of this.items || []) { + if (it && !it.dead && it.type === "grass" && !this.isGrassLimitExempt?.(it)) total += 1; + } + return total; + }, + + canAddGrass(extra = 1) { + const limit = this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 360); + if (!Number.isFinite(limit)) return true; + return (this.liveGrassCount ? this.liveGrassCount() : 0) + Math.max(0, Number(extra) || 0) <= limit; + }, + sortedDrawList() { if (!this.drawList) this.drawList = []; const antCount = liveCount(this.ants); @@ -149,6 +163,10 @@ ensureSpatial(reason = "read") { if (!this.spatial) return false; if (!this.spatialDirty && (this.spatialVersion || 0) > 0) return false; + // Rebuild at most once per frame. Queries after movement may use the previous + // frame's grid until the next update-start; this is acceptable for this game + // and avoids repeated full-grid rebuilds under dense scenes. + if ((this.spatialRebuildsThisFrame || 0) > 0 && (this.spatialVersion || 0) > 0) return false; return this.rebuildSpatial(true, reason); }, @@ -213,6 +231,9 @@ addItem(item, reason = "add-item", opts = {}) { if (!item) return null; + const ignoreGrassLimit = Boolean(opts.ignoreGrassLimit || opts.manualGrass || item.grassLimitExempt || item.manualGrass || item.placedByPlayer); + if (item.type === "grass" && !ignoreGrassLimit && !this.canAddGrass?.(1)) return null; + if (item.type === "grass" && ignoreGrassLimit) item.grassLimitExempt = true; this.items.push(item); this.markItemBucketsDirty?.(reason); this.markSpatialDirty?.(reason); @@ -273,7 +294,6 @@ }, compactItems() { - this.enforceItemBudget(); const before = this.items.length; let write = 0; for (let read = 0; read < this.items.length; read++) { @@ -291,48 +311,8 @@ return changed; }, - enforceItemBudget() { - const pruneOldest = (type, limit) => { - if (!Number.isFinite(limit) || limit < 0) return; - const live = this.liveItemsOfType?.(type) || (this.items || []).filter(it => it && it.type === type && !it.dead); - const count = live.length; - if (count <= limit) return; - const victims = live - .slice() - .sort((a, b) => (b.age || 0) - (a.age || 0)) - .slice(0, count - limit); - for (const it of victims) it.amount = 0; - }; - pruneOldest("zunchi", CONFIG.zunchiLimit ?? 56); - pruneOldest("trace", CONFIG.traceLimit ?? 64); - pruneOldest("splat", CONFIG.splatLimit ?? 48); - pruneOldest("grass", this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 96)); - const fenceLimit = this.fenceLimit ? this.fenceLimit() : (CONFIG.fenceLimit ?? 72); - const liveFences = this.liveFenceCount ? this.liveFenceCount() : 0; - if (liveFences > fenceLimit) { - const victims = (this.items || []) - .filter(it => it && this.isFenceType?.(it.type) && !it.dead) - .sort((a, b) => (b.age || 0) - (a.age || 0)) - .slice(0, liveFences - fenceLimit); - for (const it of victims) it.amount = 0; - } - - const limit = CONFIG.itemLimit ?? Infinity; - if (!Number.isFinite(limit) || this.items.length <= limit) return; - const priority = { trace: 1, splat: 2, zunchi: 3, water: 4, grass: 5, sweet: 6, firecracker: 7, fence_v: 8, fence_h: 8 }; - const removable = (this.items || []) - .filter(it => it && !it.dead && (priority[it.type] || 99) < 99) - .sort((a, b) => (priority[a.type] || 99) - (priority[b.type] || 99) || (b.age || 0) - (a.age || 0)); - let extra = this.items.length - limit; - for (const it of removable) { - if (extra <= 0) break; - it.amount = 0; - extra -= 1; - } - }, - compactEffects() { - const limit = this.performanceQuality?.().effectLimit ?? CONFIG.effectLimit ?? 96; + const limit = CONFIG.effectLimit ?? 96; if (Number.isFinite(limit) && this.effects.length > limit) { this.effects.sort((a, b) => (a.life / Math.max(a.maxLife || 1, 0.001)) - (b.life / Math.max(b.maxLife || 1, 0.001))); const remove = this.effects.length - limit; diff --git a/js/world_update.js b/js/world_update.js index ca81f9a..6d4a001 100644 --- a/js/world_update.js +++ b/js/world_update.js @@ -3,6 +3,46 @@ (function (global) { const World = global.World; if (!World) throw new Error("World is not available for mixin: world_update.js"); + + const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker"]); + const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]); + + function itemUpdateInterval(item) { + if (!item || item.dead) return Infinity; + if (REALTIME_ITEM_TYPES.has(item.type)) return 0; + if (PIN_ITEM_TYPES.has(item.type)) return 0; + if ((item.dropTimer || 0) > 0 || Math.hypot(item.vx || 0, item.vy || 0) > 0.08) return 0; + if (item.type === "zunchi") return 2.4; + if (item.type === "grass") return 3.0 + stableUnit(item.id || item.seed || "grass", "grass-update") * 2.2; + if (item.type === "duplicator") return 2.0; + if (item.type === "water") return 1.8; + if (item.type === "trace" || item.type === "splat") return 1.2; + if (item.type === "ant_corpse") return 3.5; + if (typeof isServingFoodType === "function" && isServingFoodType(item.type)) return 5.0; + return Infinity; + } + + function updateItemsScheduled(worldRef, dt) { + let ran = 0; + for (const it of worldRef.items || []) { + if (!it || it.dead) continue; + const interval = itemUpdateInterval(it); + if (!Number.isFinite(interval)) continue; + if (interval <= 0) { + it.update(dt, worldRef); + ran += 1; + continue; + } + it._scheduledUpdateDt = Math.min(10, (it._scheduledUpdateDt || 0) + dt); + if (it._scheduledUpdateDt < interval) continue; + const scheduledDt = it._scheduledUpdateDt; + it._scheduledUpdateDt = 0; + it.update(scheduledDt, worldRef); + ran += 1; + } + return ran; + } + function markSpatialDirtyIfMoved(worldRef, list, reason = "moved", threshold = 0.35) { if (!worldRef || !Array.isArray(list) || !list.length) return false; const thresholdSq = threshold * threshold; @@ -30,7 +70,9 @@ return moved; } Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ - environmentScores() { + 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; @@ -52,7 +94,9 @@ 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 }; + this.environmentScoreCache = { security, hygiene, happiness }; + this.nextEnvironmentScoreAt = nowScore + 3.0; + return this.environmentScoreCache; }, @@ -186,7 +230,7 @@ if (d < bestD && d <= Math.max(60, (it.r || 24) * 2.0)) { best = it; bestD = d; } } if (!best) return null; - const owner = (this.tarinai || []).find(t => t && !t.dead && t.id === best.ownerId) || null; + const owner = this.liveTarinaiById?.(best.ownerId) || null; return { bed: best, owner }; }, @@ -253,11 +297,9 @@ this.spatialDirtyMarksThisFrame = 0; this.ensureSpatial?.("update-start"); - const quality = this.performanceQuality?.() || {}; - const perf = Number(quality.level ?? this.performanceLevel?.() ?? 0) || 0; - this.beginFramePerformanceBudgets?.(quality); + this.beginFramePerformanceBudgets?.(); const itemCountBefore = this.items.length; - for (const it of this.items) it.update(dt, this); + updateItemsScheduled(this, dt); const itemsMoved = markSpatialDirtyIfMoved(this, this.items, "items-moved", 0.25); if (this.itemCounts?.ball) { this.ensureSpatial?.("ball-ball-collisions"); @@ -274,7 +316,7 @@ t.update(dt); } const now = this.time || 0; - const bedConflictInterval = Number(quality.bedConflictInterval || 0.12) || 0.12; + const bedConflictInterval = 3.2; if (now >= (this.nextBedConflictCheckAt || 0)) { this.nextBedConflictCheckAt = now + bedConflictInterval; this.workStats && (this.workStats.bedConflictRuns = (this.workStats.bedConflictRuns || 0) + 1); @@ -286,7 +328,7 @@ this.ensureSpatial?.("post-tarinai-update"); this.limitBallChasers(); this.resolveBallInteractions(dt); - const collisionInterval = Number(quality.collisionInterval || 0.04) || 0.04; + const collisionInterval = 1.8; if (now >= (this.nextTarinaiCollisionCheckAt || 0)) { this.nextTarinaiCollisionCheckAt = now + collisionInterval; this.workStats && (this.workStats.collisionRuns = (this.workStats.collisionRuns || 0) + 1); @@ -296,7 +338,7 @@ } this.compactTimer += dt; - const compactInterval = perf >= 2 ? 0.95 : 0.58; + const compactInterval = 2.5; let itemsCompacted = false; if (this.compactTimer >= compactInterval) { itemsCompacted = this.compactItems(); @@ -307,20 +349,20 @@ } this.countsTimer += dt; - const countsInterval = perf >= 2 ? 0.85 : 0.42; + const countsInterval = 2.0; if (this.countsTimer >= countsInterval || itemsCompacted || this.items.length !== itemCountBefore) { this.updateItemCounts(); this.updateEffectCounts(); this.countsTimer = 0; } - if ((this.outOfBoundsCheckAt || -999) + 1.0 <= this.time) { + if ((this.outOfBoundsCheckAt || -999) + 5.0 <= this.time) { this.outOfBoundsCheckAt = this.time; this.sanitizeOutOfBounds(); } this.drawSortTimer += dt; - const sortInterval = perf >= 2 ? 0.16 : perf === 1 ? 0.11 : 0.07; + const sortInterval = 0.25; if (this.drawSortTimer >= sortInterval) { this.drawListDirty = true; this.drawSortTimer = 0; @@ -343,8 +385,7 @@ } - const grassCount = this.itemCounts.grass || 0; - if (grassCount < Math.min(108, this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 360)) && Math.random() < dt * 0.109) { + if (Math.random() < dt * 0.109) { const spot = this.findGrassPlantingSpot(rand(60, this.w - 60), rand(60, this.h - 60), { allowOriginal: true, minRadius: 28, @@ -355,7 +396,8 @@ const sprout = new Item("grass", spot.x, spot.y); sprout.amount = rand(30, 62); sprout.growth = rand(0.18, 0.38); - this.addItem?.(sprout, "grass-spawn") || this.items.push(sprout); + if (this.addItem) this.addItem(sprout, "grass-spawn"); + else this.items.push(sprout); } } @@ -377,28 +419,28 @@ 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"; + 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 = (this.w || 1000) - pad + radius; + const maxX = safeW - pad + radius; const minY = pad - radius; - const maxY = (this.h || 720) - pad + radius; + const maxY = safeH - 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); + 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 === "delete") { it.amount = 0; changedItems = true; } - else if (result === "moved") changedItems = true; + if (result === "moved") changedItems = true; } for (const ef of this.effects || []) { - const result = repairPoint(ef, ef.size || 12); - if (result === "delete") ef.life = 0; + 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); @@ -412,18 +454,34 @@ }; 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 === "delete" || result === "moved") repairTarinai(t); + 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 === "delete") { - ant.dead = true; - changedAnts = true; - continue; - } + if (result === "missing") continue; if (result === "moved") { ant.vx = 0; ant.vy = 0; diff --git a/js/world_view.js b/js/world_view.js index 74ebc3f..1122516 100644 --- a/js/world_view.js +++ b/js/world_view.js @@ -67,8 +67,7 @@ this.markTerrainDirty("terrain-signature"); return; } - const q = window.TarinaiPerformance?.quality?.(this) || {}; - const interval = q.terrainRedrawInterval || (this.performanceLevel() >= 2 ? 1.05 : 0.62); + const interval = 3.0; if ((this.time || 0) - (this.terrainLastDirtyAt || 0) >= interval) this.markTerrainDirty("terrain-periodic"); }, @@ -98,23 +97,6 @@ return Math.max(0.22, field.worldScale || 1); }, - grassLimit() { - const field = this.fieldDefinition(); - if (Number.isFinite(field?.grassLimit)) return Math.max(0, Math.round(field.grassLimit)); - const base = CONFIG.grassLimit ?? 96; - const mediumScale = Math.max(0.22, FIELD_TYPES?.garden?.worldScale || 1); - const ratio = this.fieldWorldScale() / mediumScale; - // \u4e2d\u30b5\u30a4\u30ba\u3092 CONFIG.grassLimit \u306e\u57fa\u6e96\u306b\u3057\u3001\u30d5\u30a3\u30fc\u30eb\u30c9\u306e\u8ad6\u7406\u30b5\u30a4\u30ba\u306b\u5fdc\u3058\u3066\u5897\u6e1b\u3059\u308b\u3002 - return Math.max(12, Math.round(base * ratio)); - }, - - fenceLimit() { - const base = CONFIG.fenceLimit ?? 72; - const mediumScale = Math.max(0.22, FIELD_TYPES?.garden?.worldScale || 1); - const ratio = Math.max(0.24, this.fieldWorldScale() / mediumScale); - return clamp(Math.round(base * Math.sqrt(ratio)), 28, 140); - }, - isFenceType(type = "") { return type === "fence_v" || type === "fence_h"; }, diff --git a/service-worker.js b/service-worker.js index fd5be6e..8f1f946 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,6 +1,6 @@ "use strict"; -const APP_VERSION = "15.24.53"; +const APP_VERSION = "16.00.01"; const CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const v = `v=${APP_VERSION}`; // Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.