This commit is contained in:
33333-33333 2026-06-24 21:20:53 +09:00
commit 2741b93dea
31 changed files with 751 additions and 546 deletions

54
LAG_OPTIMIZATION_NOTES.md Normal file
View file

@ -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.

View file

@ -1,3 +1,10 @@
# 15.52.00 - 草上限復活・日光浴描画修正
- 草だけ上限を復活。上限到達後は、手動配置と自然繁殖の新規追加を止める。既存草は上限処理では削除しない。
- 日光浴開始位置をアンカー化し、発生後に中央へ寄らないよう固定。
- 日光浴スプライト描画を通常描画経路に一本化し、未ロード時は透明化せず読み込み済み候補または通常表情へフォールバック。
- 到達不能だった日光浴専用直接描画メソッドと、未使用のアイテム上限メソッドを削除。
# 15.24.53 # 15.24.53
- 排泄直後のずんちを通常個体が即座に食べないよう、ずんち専用の摂食保護時間を追加。 - 排泄直後のずんちを通常個体が即座に食べないよう、ずんち専用の摂食保護時間を追加。
@ -23,3 +30,23 @@
- 日光浴中のスプライトを、通常表情ロックとは別枠で固定。 - 日光浴中のスプライトを、通常表情ロックとは別枠で固定。
- sunbath 系スプライトを初期ロード対象へ追加。 - 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.

View file

@ -1,5 +1,5 @@
{ {
"version": "15.24.53", "version": "16.00.01",
"css": [ "css": [
"css/base.css", "css/base.css",
"css/layout.css", "css/layout.css",

Binary file not shown.

View file

@ -5,11 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>&#x305F;&#x308A;&#x306A;&#x3044;&#x89B3;&#x5BDF;</title> <title>&#x305F;&#x308A;&#x306A;&#x3044;&#x89B3;&#x5BDF;</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 64 64%27%3E%3Ctext y=%2752%27 font-size=%2752%27%3E%E3%81%9F%3C/text%3E%3C/svg%3E" /> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 64 64%27%3E%3Ctext y=%2752%27 font-size=%2752%27%3E%E3%81%9F%3C/text%3E%3C/svg%3E" />
<link rel="stylesheet" href="css/base.css?v=15.24.53" /> <link rel="stylesheet" href="css/base.css?v=16.00.01" />
<link rel="stylesheet" href="css/layout.css?v=15.24.53" /> <link rel="stylesheet" href="css/layout.css?v=16.00.01" />
<link rel="stylesheet" href="css/panel.css?v=15.24.53" /> <link rel="stylesheet" href="css/panel.css?v=16.00.01" />
<link rel="stylesheet" href="css/components.css?v=15.24.53" /> <link rel="stylesheet" href="css/components.css?v=16.00.01" />
<link rel="stylesheet" href="css/mobile.css?v=15.24.53" /> <link rel="stylesheet" href="css/mobile.css?v=16.00.01" />
</head> </head>
<body> <body>
<div id="loadingScreen" class="loading-screen" aria-live="polite"> <div id="loadingScreen" class="loading-screen" aria-live="polite">
@ -183,69 +183,69 @@
</div> </div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div> <div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=15.24.53" defer></script> <script src="js/version.js?v=16.00.01" defer></script>
<script src="js/event_bus.js?v=15.24.53" defer></script> <script src="js/event_bus.js?v=16.00.01" defer></script>
<script src="js/performance_manager.js?v=15.24.53" defer></script> <script src="js/performance_manager.js?v=16.00.01" defer></script>
<script src="js/registry_base.js?v=15.24.53" defer></script> <script src="js/registry_base.js?v=16.00.01" defer></script>
<script src="js/disease_registry.js?v=15.24.53" defer></script> <script src="js/disease_registry.js?v=16.00.01" defer></script>
<script src="js/effect_registry.js?v=15.24.53" defer></script> <script src="js/effect_registry.js?v=16.00.01" defer></script>
<script src="js/sound_pack.js?v=15.24.53" defer></script> <script src="js/sound_pack.js?v=16.00.01" defer></script>
<script src="js/data.js?v=15.24.53" defer></script> <script src="js/data.js?v=16.00.01" defer></script>
<script src="js/item_visual_registry.js?v=15.24.53" defer></script> <script src="js/item_visual_registry.js?v=16.00.01" defer></script>
<script src="js/food_registry.js?v=15.24.53" defer></script> <script src="js/food_registry.js?v=16.00.01" defer></script>
<script src="js/input_mode_manager.js?v=15.24.53" defer></script> <script src="js/input_mode_manager.js?v=16.00.01" defer></script>
<script src="js/math.js?v=15.24.53" defer></script> <script src="js/math.js?v=16.00.01" defer></script>
<script src="js/physics_helpers.js?v=15.24.53" defer></script> <script src="js/physics_helpers.js?v=16.00.01" defer></script>
<script src="js/assets.js?v=15.24.53" defer></script> <script src="js/assets.js?v=16.00.01" defer></script>
<script src="js/audio.js?v=15.24.53" defer></script> <script src="js/audio.js?v=16.00.01" defer></script>
<script src="js/render.js?v=15.24.53" defer></script> <script src="js/render.js?v=16.00.01" defer></script>
<script src="js/sim_core.js?v=15.24.53" defer></script> <script src="js/sim_core.js?v=16.00.01" defer></script>
<script src="js/structures.js?v=15.24.53" defer></script> <script src="js/structures.js?v=16.00.01" defer></script>
<script src="js/items.js?v=15.24.53" defer></script> <script src="js/items.js?v=16.00.01" defer></script>
<script src="js/ants.js?v=15.24.53" defer></script> <script src="js/ants.js?v=16.00.01" defer></script>
<script src="js/health.js?v=15.24.53" defer></script> <script src="js/health.js?v=16.00.01" defer></script>
<script src="js/tarinai.js?v=15.24.53" defer></script> <script src="js/tarinai.js?v=16.00.01" defer></script>
<script src="js/tarinai_action_spec.js?v=15.24.53" defer></script> <script src="js/tarinai_action_spec.js?v=16.00.01" defer></script>
<script src="js/tarinai_behavior_state.js?v=15.24.53" defer></script> <script src="js/tarinai_behavior_state.js?v=16.00.01" defer></script>
<script src="js/tarinai_identity_social.js?v=15.24.53" defer></script> <script src="js/tarinai_identity_social.js?v=16.00.01" defer></script>
<script src="js/tarinai_action_state.js?v=15.24.53" defer></script> <script src="js/tarinai_action_state.js?v=16.00.01" defer></script>
<script src="js/tarinai_disease_nest.js?v=15.24.53" defer></script> <script src="js/tarinai_disease_nest.js?v=16.00.01" defer></script>
<script src="js/tarinai_item_effects.js?v=15.24.53" defer></script> <script src="js/tarinai_item_effects.js?v=16.00.01" defer></script>
<script src="js/tarinai_needs_items.js?v=15.24.53" defer></script> <script src="js/tarinai_needs_items.js?v=16.00.01" defer></script>
<script src="js/tarinai_social_move_life.js?v=15.24.53" defer></script> <script src="js/tarinai_social_move_life.js?v=16.00.01" defer></script>
<script src="js/tarinai_render.js?v=15.24.53" defer></script> <script src="js/tarinai_render.js?v=16.00.01" defer></script>
<script src="js/world.js?v=15.24.53" defer></script> <script src="js/world.js?v=16.00.01" defer></script>
<script src="js/world_view.js?v=15.24.53" defer></script> <script src="js/world_view.js?v=16.00.01" defer></script>
<script src="js/family_graph.js?v=15.24.53" defer></script> <script src="js/family_graph.js?v=16.00.01" defer></script>
<script src="js/world_family_social.js?v=15.24.53" defer></script> <script src="js/world_family_social.js?v=16.00.01" defer></script>
<script src="js/world_combat_effects.js?v=15.24.53" defer></script> <script src="js/world_combat_effects.js?v=16.00.01" defer></script>
<script src="js/world_environment.js?v=15.24.53" defer></script> <script src="js/world_environment.js?v=16.00.01" defer></script>
<script src="js/world_spatial_budget.js?v=15.24.53" defer></script> <script src="js/world_spatial_budget.js?v=16.00.01" defer></script>
<script src="js/world_ants_system.js?v=15.24.53" defer></script> <script src="js/world_ants_system.js?v=16.00.01" defer></script>
<script src="js/weather_system.js?v=15.24.53" defer></script> <script src="js/weather_system.js?v=16.00.01" defer></script>
<script src="js/world_update.js?v=15.24.53" defer></script> <script src="js/world_update.js?v=16.00.01" defer></script>
<script src="js/world_placement_log.js?v=15.24.53" defer></script> <script src="js/world_placement_log.js?v=16.00.01" defer></script>
<script src="js/world_bootstrap.js?v=15.24.53" defer></script> <script src="js/world_bootstrap.js?v=16.00.01" defer></script>
<script src="js/text_catalog.js?v=15.24.53" defer></script> <script src="js/text_catalog.js?v=16.00.01" defer></script>
<script src="js/ui.js?v=15.24.53" defer></script> <script src="js/ui.js?v=16.00.01" defer></script>
<script src="js/ui_log.js?v=15.24.53" defer></script> <script src="js/ui_log.js?v=16.00.01" defer></script>
<script src="js/ui_helpers.js?v=15.24.53" defer></script> <script src="js/ui_helpers.js?v=16.00.01" defer></script>
<script src="js/ui_selected.js?v=15.24.53" defer></script> <script src="js/ui_selected.js?v=16.00.01" defer></script>
<script src="js/snapshot_system.js?v=15.24.53" defer></script> <script src="js/snapshot_system.js?v=16.00.01" defer></script>
<script src="js/save_system.js?v=15.24.53" defer></script> <script src="js/save_system.js?v=16.00.01" defer></script>
<script src="js/patch_helpers.js?v=15.24.53" defer></script> <script src="js/patch_helpers.js?v=16.00.01" defer></script>
<script src="js/freeze_system.js?v=15.24.53" defer></script> <script src="js/freeze_system.js?v=16.00.01" defer></script>
<script src="js/relation_event_system.js?v=15.24.53" defer></script> <script src="js/relation_event_system.js?v=16.00.01" defer></script>
<script src="js/ui_layout_dialogs.js?v=15.24.53" defer></script> <script src="js/ui_layout_dialogs.js?v=16.00.01" defer></script>
<script src="js/ui_tools.js?v=15.24.53" defer></script> <script src="js/ui_tools.js?v=16.00.01" defer></script>
<script src="js/ui_bind.js?v=15.24.53" defer></script> <script src="js/ui_bind.js?v=16.00.01" defer></script>
<script src="js/ui_charts.js?v=15.24.53" defer></script> <script src="js/ui_charts.js?v=16.00.01" defer></script>
<script src="js/ui_family_data.js?v=15.24.53" defer></script> <script src="js/ui_family_data.js?v=16.00.01" defer></script>
<script src="js/ui_family_async.js?v=15.24.53" defer></script> <script src="js/ui_family_async.js?v=16.00.01" defer></script>
<script src="js/ui_family_layout.js?v=15.24.53" defer></script> <script src="js/ui_family_layout.js?v=16.00.01" defer></script>
<script src="js/ui_family_paths.js?v=15.24.53" defer></script> <script src="js/ui_family_paths.js?v=16.00.01" defer></script>
<script src="js/ui_family_render.js?v=15.24.53" defer></script> <script src="js/ui_family_render.js?v=16.00.01" defer></script>
<script src="js/main.js?v=15.24.53" defer></script> <script src="js/main.js?v=16.00.01" defer></script>
<script src="js/debug_tools.js?v=15.24.53" defer></script> <script src="js/debug_tools.js?v=16.00.01" defer></script>
</body> </body>
</html> </html>

View file

@ -94,7 +94,8 @@ class AntActor {
} }
homeNest() { 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() { adoptableNest() {
@ -165,7 +166,8 @@ class AntActor {
// filter or rank candidates here while preserving sleep-disease hauling. // filter or rank candidates here while preserving sleep-disease hauling.
let best = null; let best = null;
let bestScore = Infinity; 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; if (!t || t.dead || t.insideNestBoxId || t.isZunchiSlave) continue;
const d = distXY(this.x, this.y, t.x, t.y); const d = distXY(this.x, this.y, t.x, t.y);
if (d > maxDist) continue; if (d > maxDist) continue;

View file

@ -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("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\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("\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; return false;
}, },
eat() { if (!this.playSample("sfx_eat", 0.20, "voice")) this.play("sfx_eat", { category: "voice", minGap: 0.20 }); }, eat() { if (!this.playSample("sfx_eat", 0.20, "voice")) this.play("sfx_eat", { category: "voice", minGap: 0.20 }); },

View file

@ -235,13 +235,9 @@ const NEEDS = ["\u98df\u3079\u7269", "\u4ef2\u9593", "\u7720\u308a", "\u52c7\u6c
const CONFIG = { const CONFIG = {
initialPopulation: 18, initialPopulation: 18,
maxPopulation: Number.MAX_SAFE_INTEGER, maxPopulation: Number.MAX_SAFE_INTEGER,
itemLimit: 260,
fenceLimit: 72,
maxFenceCollisionChecks: 18, maxFenceCollisionChecks: 18,
zunchiLimit: 56,
traceLimit: 64,
splatLimit: 48,
effectLimit: 78, effectLimit: 78,
grassLimit: 360,
worldPadding: 30, worldPadding: 30,
hungerPerMinute: 36.0, hungerPerMinute: 36.0,
lonelinessPerMinute: 7.6, lonelinessPerMinute: 7.6,
@ -255,7 +251,6 @@ const CONFIG = {
weatherChangeMin: 46, weatherChangeMin: 46,
weatherChangeMax: 96, weatherChangeMax: 96,
bubbleLimit: 32, bubbleLimit: 32,
grassLimit: 360,
highPopulationMode: 70, highPopulationMode: 70,
ultraPopulationMode: 120, ultraPopulationMode: 120,
}; };

View file

@ -24,11 +24,11 @@
const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {}; const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {};
const behaviorCounts = diag.behavior?.counts || {}; 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 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 = [ panel.textContent = [
`tarinai v${window.TARINAI_VERSION || "?"}`, `tarinai v${window.TARINAI_VERSION || "?"}`,
`fps ${perf.fps ?? window.__tarinaiFps ?? "?"} perf ${perf.level ?? "?"}`, `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}`, `items ${(w?.items || []).length} tarinai ${(w?.tarinai || []).length} ants ${(w?.ants || []).length}`,
`effects ${(w?.effects || []).length}`, `effects ${(w?.effects || []).length}`,
`terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0}`, `terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0}`,

View file

@ -943,7 +943,8 @@ class Item {
let best = null; let best = null;
let bestD = Infinity; let bestD = Infinity;
const hitRange = Math.max(10, (this.r || 8) * 1.25); 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 (!t || t.dead) continue;
if (this.noStickTargetId && this.noStickTargetId === t.id && (this.noStickTargetUntil || 0) > now) continue; if (this.noStickTargetId && this.noStickTargetId === t.id && (this.noStickTargetUntil || 0) > now) continue;
const d = distXY(this.x, this.y, t.x, t.y); const d = distXY(this.x, this.y, t.x, t.y);
@ -1005,7 +1006,7 @@ class Item {
} }
updateLodgedPushpin(dt, worldRef) { 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) { if (!t) {
this.detachPushpin(worldRef, "owner-lost"); this.detachPushpin(worldRef, "owner-lost");
return; return;
@ -1055,7 +1056,7 @@ class Item {
} }
detachPushpin(worldRef, reason = "released") { 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 behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(this.type) : null;
const wasOshibyo = Boolean(behavior?.blocksZunchi); const wasOshibyo = Boolean(behavior?.blocksZunchi);
const storedZunchi = wasOshibyo && t ? Math.max(0, Math.floor(t.oshiriByoZunchiStock || 0)) : 0; 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); 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.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); 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); const grassCanSpread = worldRef.canAddGrass ? worldRef.canAddGrass(1) : ((worldRef.itemCounts?.grass || 0) < (CONFIG.grassLimit ?? 360));
if (this.growth >= 0.86 && (worldRef.itemCounts?.grass || 0) < grassLimit) { if (this.growth >= 0.86 && grassCanSpread) {
const spawned = this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 2); const spawned = this.seedAround(worldRef, fertileSource, this.fertilityBoost > 0.25, 2);
if (spawned > 0) this.growth = rand(0.52, 0.68); if (spawned > 0) this.growth = rand(0.52, 0.68);
this.seedTimer = rand(9, 17); this.seedTimer = rand(9, 17);
@ -1157,15 +1158,15 @@ class Item {
this.amount = clamp(this.growth * 120 * this.health, 0, 130); 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); 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); 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); 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) { seedAround(worldRef, fertileSource = null, fertile = false, maxNew = 1) {
let made = 0; let made = 0;
const limit = worldRef.grassLimit ? worldRef.grassLimit() : (CONFIG.grassLimit ?? 96); for (let n = 0; n < maxNew && (worldRef.canAddGrass ? worldRef.canAddGrass(1) : ((worldRef.itemCounts?.grass || 0) < (CONFIG.grassLimit ?? 360))); n++) {
for (let n = 0; n < maxNew && (worldRef.itemCounts?.grass || 0) < limit; n++) {
const source = fertile && fertileSource ? fertileSource : this; const source = fertile && fertileSource ? fertileSource : this;
const spot = worldRef.findGrassSproutSpot const spot = worldRef.findGrassSproutSpot
? worldRef.findGrassSproutSpot(source, Boolean(fertile && fertileSource)) ? worldRef.findGrassSproutSpot(source, Boolean(fertile && fertileSource))
@ -1174,7 +1175,8 @@ class Item {
const sprout = new Item("grass", spot.x, spot.y); const sprout = new Item("grass", spot.x, spot.y);
sprout.growth = rand(0.08, 0.18); sprout.growth = rand(0.08, 0.18);
sprout.amount = sprout.growth * 120; 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; made += 1;
} }
return made; return made;
@ -1219,7 +1221,7 @@ class Item {
const styleNight = lightState.light < 0.42; const styleNight = lightState.light < 0.42;
const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55); const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55);
if (isPinType(this.type) && this.pinState === "lodged") { 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; if (owner?.insideNestBoxId) return;
} }
const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null; const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null;

View file

@ -3,9 +3,6 @@ let last = nowSec();
let statsTimer = 0; let statsTimer = 0;
let fpsTimer = 0; let fpsTimer = 0;
let fpsFrames = 0; let fpsFrames = 0;
let perfResizeTimer = 0;
let lastPerfLevel = 0;
let lastPerfMaxDpr = 0;
window.__tarinaiFps = 0; window.__tarinaiFps = 0;
const LOADING_SPRITES = [ const LOADING_SPRITES = [
@ -85,26 +82,14 @@ function loop() {
fpsFrames += 1; fpsFrames += 1;
if (fpsTimer >= 0.5) { if (fpsTimer >= 0.5) {
window.__tarinaiFps = Math.round(fpsFrames / Math.max(fpsTimer, 0.001)); window.__tarinaiFps = Math.round(fpsFrames / Math.max(fpsTimer, 0.001));
window.TarinaiPerformance?.updateFps?.(window.__tarinaiFps, world); window.TarinaiPerformance?.updateFps?.(window.__tarinaiFps);
fpsTimer = 0; fpsTimer = 0;
fpsFrames = 0; fpsFrames = 0;
} }
world.update(dt); world.update(dt);
render(); render();
statsTimer += dt; statsTimer += dt;
const quality = world.performanceQuality?.() || {}; const statsInterval = 2.5;
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);
if (statsTimer > statsInterval) { if (statsTimer > statsInterval) {
renderStats(); renderStats();
statsTimer = 0; statsTimer = 0;

View file

@ -1,100 +1,55 @@
"use strict"; "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 { class TarinaiPerformanceManager {
constructor() { constructor() {
this.fps = 60; this.fps = 60;
this.level = 0; this.level = 0;
this.lastLevel = 0; this.lastLevel = 0;
this.samples = [];
this.sampleLimit = 20;
this.pressure = 0;
} }
updateFps(fps, worldRef = null) { updateFps(fps) {
const n = Number(fps); const n = Number(fps);
if (!Number.isFinite(n) || n <= 0) return this.level; if (Number.isFinite(n) && n > 0) this.fps = Math.round(n);
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;
return 0; return 0;
} }
computePressure(fps = this.fps, worldRef = null) { quality() {
const q = this.countWorkload(worldRef); return TarinaiPerformanceManager.STATIC_QUALITY;
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);
} }
countWorkload(worldRef = null) { debugSnapshot() {
let tarinaiCount = 0; return { version: window.TARINAI_VERSION || "", fps: this.fps, level: 0, quality: TarinaiPerformanceManager.STATIC_QUALITY };
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) };
} }
} }
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(); window.TarinaiPerformance = window.TarinaiPerformance || new TarinaiPerformanceManager();

View file

@ -704,9 +704,7 @@ function drawAtmosphericLighting(ctx, w, h, lighting) {
function drawRain(ctx, w, h, t) { function drawRain(ctx, w, h, t) {
if (world.weather !== "light_rain") return; if (world.weather !== "light_rain") return;
ctx.save(); ctx.save();
const quality = world.performanceQuality?.() || {}; const count = 96;
const density = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1);
const count = Math.max(20, Math.round(138 * density));
const angle = -0.32; // Rain visual effect: diagonal streaks, separate from fallen water item sprites. const angle = -0.32; // Rain visual effect: diagonal streaks, separate from fallen water item sprites.
const slant = Math.tan(angle); const slant = Math.tan(angle);
for (let i = 0; i < count; i++) { 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.moveTo(x - dx * 0.5, y - dy * 0.5);
ctx.lineTo(x + dx * 0.5, y + dy * 0.5); ctx.lineTo(x + dx * 0.5, y + dy * 0.5);
ctx.stroke(); ctx.stroke();
if (density > 0.25 && i % 4 === 0) { if (i % 4 === 0) {
ctx.globalAlpha = 0.13 * alpha; ctx.globalAlpha = 0.13 * alpha;
ctx.strokeStyle = "rgba(255,255,255,0.70)"; ctx.strokeStyle = "rgba(255,255,255,0.70)";
ctx.lineWidth = 0.7; ctx.lineWidth = 0.7;
@ -796,9 +794,7 @@ function render() {
} }
ctx.restore(); ctx.restore();
const quality = world.performanceQuality?.() || {}; drawLightRays(ctx, screenW, screenH, lighting);
const density = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1);
if (density > 0.45) drawLightRays(ctx, screenW, screenH, lighting);
const visibleRect = visibleWorldRect(world, 180); const visibleRect = visibleWorldRect(world, 180);
const terrainLayer = ensureTerrainCache(world, lighting); const terrainLayer = ensureTerrainCache(world, lighting);

View file

@ -264,7 +264,7 @@
function itemExtra(item, tarinaiIndex = new Map()) { function itemExtra(item, tarinaiIndex = new Map()) {
const type = item?.type || ""; 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 (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 === "bed") return [q(item.comfort, 1000), q(item.wear, 1000)];
if (type === "signboard") return [item.text || ""]; if (type === "signboard") return [item.text || ""];
if (type === "duplicator") return [item.storedFoodType || "", item.storedFoodLabel || ""]; if (type === "duplicator") return [item.storedFoodType || "", item.storedFoodLabel || ""];
@ -306,7 +306,7 @@
return; return;
} }
if (type === "grass") { 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") { } else if (type === "bed") {
item.comfort = u(extra[0], 1000); item.wear = u(extra[1], 1000); item.comfort = u(extra[0], 1000); item.wear = u(extra[1], 1000);
} else if (type === "signboard") { } else if (type === "signboard") {

View file

@ -3,7 +3,7 @@
(function (global) { (function (global) {
// External audio replacement point. Add files and edit paths here; audio.js consumes this pack at startup. // External audio replacement point. Add files and edit paths here; audio.js consumes this pack at startup.
global.TARINAI_SOUND_PACK = Object.freeze({ global.TARINAI_SOUND_PACK = Object.freeze({
version: "1.1", version: "1.2",
categoryGain: { voice: 3.2, notify: 18.0, ops: 16.0 }, categoryGain: { voice: 3.2, notify: 18.0, ops: 16.0 },
idGain: { idGain: {
sfx_notify: 2.60, sfx_notify: 2.60,
@ -30,6 +30,7 @@
sfx_ant_die: 0.14, sfx_ant_die: 0.14,
sfx_notify: 0.10, sfx_notify: 0.10,
sfx_log_push: 0.10, sfx_log_push: 0.10,
voice_sunbath: 0.95,
}, },
samplePaths: { samplePaths: {
voice_hau: "assets/sounds/voice_001_hau.wav", voice_hau: "assets/sounds/voice_001_hau.wav",
@ -38,6 +39,7 @@
sfx_eat: "assets/sounds/voice_004_eat.wav", sfx_eat: "assets/sounds/voice_004_eat.wav",
voice_stress: "assets/sounds/voice_005_stress.wav", voice_stress: "assets/sounds/voice_005_stress.wav",
sfx_sleep: "assets/sounds/voice_006_sleep.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" sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3"
}, },
categories: { categories: {
@ -46,7 +48,7 @@
ops: "\u64cd\u4f5c" ops: "\u64cd\u4f5c"
}, },
soundMap: { 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_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_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", sfx_disease: "voice", sfx_heal: "voice", sfx_ant_drag: "voice", sfx_ant_die: "voice", sfx_queen_ant: "voice",

View file

@ -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; 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.x = opts.x ?? rand(100, world.w - 100);
this.y = opts.y ?? rand(100, world.h - 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.vx = opts.vx ?? rand(-12, 12);
this.vy = opts.vy ?? rand(-12, 12); this.vy = opts.vy ?? rand(-12, 12);
this.impulseVx = opts.impulseVx ?? 0; this.impulseVx = opts.impulseVx ?? 0;
@ -123,6 +128,8 @@ class Tarinai { constructor(world, opts = {}) {
this.sunbathCooldown = opts.sunbathCooldown ?? 0; this.sunbathCooldown = opts.sunbathCooldown ?? 0;
this.sunbathFrameTimer = opts.sunbathFrameTimer ?? 0; this.sunbathFrameTimer = opts.sunbathFrameTimer ?? 0;
this.sunbathSpriteVariant = opts.sunbathSpriteVariant ?? 1; 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.intimidateTimer = opts.intimidateTimer ?? 0;
this.intimidateTargetId = opts.intimidateTargetId || null; this.intimidateTargetId = opts.intimidateTargetId || null;
this.intimidatedTimer = opts.intimidatedTimer ?? 0; this.intimidatedTimer = opts.intimidatedTimer ?? 0;
@ -174,7 +181,10 @@ class Tarinai { constructor(world, opts = {}) {
this.fightTargetId = opts.fightTargetId || null; this.fightTargetId = opts.fightTargetId || null;
this.fightTargetIds = Array.isArray(opts.fightTargetIds) ? [...opts.fightTargetIds] : (this.fightTargetId ? [this.fightTargetId] : []); this.fightTargetIds = Array.isArray(opts.fightTargetIds) ? [...opts.fightTargetIds] : (this.fightTargetId ? [this.fightTargetId] : []);
this.nextHeadbutt = opts.nextHeadbutt ?? 0; 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.poopCount = opts.poopCount ?? 0;
this.oshiriByoZunchiStock = opts.oshiriByoZunchiStock ?? 0; this.oshiriByoZunchiStock = opts.oshiriByoZunchiStock ?? 0;
this.stretchTimer = opts.stretchTimer ?? 0; this.stretchTimer = opts.stretchTimer ?? 0;

View file

@ -285,10 +285,8 @@
this.poopCount = (this.poopCount || 0) + 1; this.poopCount = (this.poopCount || 0) + 1;
this.digest = Math.max(0, (this.digest || 0) - 3.0); this.digest = Math.max(0, (this.digest || 0) - 3.0);
const now = this.world?.time || 0; const now = this.world?.time || 0;
if (now >= (this.nextPoopBubbleAt || -999)) { this.nextPoopBubbleAt = now + 2.2;
this.nextPoopBubbleAt = now + 2.2; this.bubble("\u3076\u308a\u3085\u3063", 1.2, ITEM_EFFECT_COLORS.laxative, { force: true });
this.bubble("\u3076\u308a\u3085\u3063", 1.2, ITEM_EFFECT_COLORS.laxative);
}
if ((this.world.time || 0) > (this.nextLaxativeLogAt || -999)) { if ((this.world.time || 0) > (this.nextLaxativeLogAt || -999)) {
this.nextLaxativeLogAt = (this.world.time || 0) + 9; 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] }); this.world.log?.(`${this.name}\u306f\u4e0b\u5264\u306e\u52b9\u679c\u3067\u305a\u3093\u3061\u3092\u653e\u51fa\u3057\u305f\u3002`, "accident", { participants: [this] });

View file

@ -262,7 +262,10 @@ function scoreNeedForAction(needs, tarinai, world, need) {
let score = value; let score = value;
if (need === "safety") score += 14; if (need === "safety") score += 14;
else if (need === "health") score += 8; 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; if ((typeof getTarinaiBehaviorNeed === "function" ? getTarinaiBehaviorNeed(tarinai) : tarinai?.behavior?.need) === need) score += 10;
const cooldown = Number(tarinai?.actionCooldowns?.[need] || 0) || 0; const cooldown = Number(tarinai?.actionCooldowns?.[need] || 0) || 0;
if (cooldown > 0) score -= Math.min(22, cooldown * 2.8); 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 (tarinai.target && !tarinai.target.dead && (!b.targetId || behaviorTargetId(tarinai.target) === b.targetId)) return tarinai.target;
if (!b.targetId) return null; if (!b.targetId) return null;
const id = b.targetId; 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?.itemById?.(id)
|| (tarinai.world?.items || []).find(item => item && !item.dead && item.id === id) || (tarinai.world?.items || []).find(item => item && !item.dead && item.id === id)
|| null; || null;
@ -492,7 +495,7 @@ function nearestForcedFightTarget(tarinai, world, maxDist = 720) {
function resolveForcedBehaviorTarget(tarinai, world, entry = {}, action = null) { function resolveForcedBehaviorTarget(tarinai, world, entry = {}, action = null) {
if (!tarinai || !world) return null; if (!tarinai || !world) return null;
if (entry.targetId) { 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.itemById?.(entry.targetId)
|| (world.items || []).find(item => item && !item.dead && item.id === entry.targetId) || (world.items || []).find(item => item && !item.dead && item.id === entry.targetId)
|| null; || null;
@ -899,6 +902,8 @@ function isDuplicatorFoodSource(item) {
return !!item && item.type === "duplicator"; return !!item && item.type === "duplicator";
} }
const NORMAL_POOP_MEAL_THRESHOLD = 3;
function duplicatorPoopMultiplier(foodType = "") { function duplicatorPoopMultiplier(foodType = "") {
if (foodType === "sweet") return 0.035; if (foodType === "sweet") return 0.035;
if (foodType === "food") return 0.040; if (foodType === "food") return 0.040;
@ -906,6 +911,13 @@ function duplicatorPoopMultiplier(foodType = "") {
return 0.042; 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) { function effectiveFoodRoleMatches(item, role) {
const type = effectiveFoodTypeForItem(item); const type = effectiveFoodTypeForItem(item);
if (!type) return false; 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 === "fight_mochi") return 2600 + Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 4;
if (type === "sleep_drug") return need === "sleep" ? 3600 : 2450; if (type === "sleep_drug") return need === "sleep" ? 3600 : 2450;
if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return 3000 + Math.max(0, healthNeed - 45) * 5; 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 === "zunchi") return 1000 + (tarinai?.isZunchiSlave ? 250 : 0);
if (type === "food" || type === "ant_corpse") return 3000; if (type === "food" || type === "ant_corpse") return 3000;
if (typeof isServingFoodType === "function" && isServingFoodType(type)) 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) { function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
let best = null, bestScore = Infinity; let best = null, bestScore = Infinity;
const hunger = Number(tarinai?.hunger || 0) || 0; 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 (!roleMatches(item, role)) continue;
if (tarinai?.shouldAvoidTarget?.(item)) continue; if (tarinai?.shouldAvoidTarget?.(item)) continue;
const d = item.type === "duplicator" ? foodInteractionDistance(tarinai, item) : dist(tarinai, item); 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); const evaluated = evaluateConflictUrge(tarinai, world, 118);
if (evaluated && evaluated.score >= minScore) return evaluated; if (evaluated && evaluated.score >= minScore) return evaluated;
const id = tarinai?.conflictTargetId || ""; 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; if (!target || !world?.canFightPair?.(tarinai, target)) return null;
const d = dist(tarinai, target); const d = dist(tarinai, target);
const carried = Math.max(0, Number(tarinai.conflictUrge || 0) || 0) - Math.max(0, d - 70) * 0.28; 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) { function tarinaiNeedCalcInterval(tarinai, world) {
const perf = world?.performanceLevel ? Number(world.performanceLevel() || 0) : 0; const jitter = typeof stableUnit === "function" ? stableUnit(tarinai?.familyKey || tarinai?.id || "tarinai", "need-calc-interval") * 0.35 : Math.random() * 0.30;
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 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; 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) { function shouldForceNeedRecalc(tarinai, world) {
@ -1595,7 +1611,7 @@ function createPanicEscapeActionSpec() {
weight: 62, weight: 62,
timerBacked: true, timerBacked: true,
selectTarget(t, world) { 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 = {}) { canStart(t, world, ctx = {}) {
return (Number(t?.needRaw?.safety ?? t?.needs?.safety ?? 0) >= 58) 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.applyFoodHungerRelief ? t.applyFoodHungerRelief(bite * 0.62) : (t.hunger -= bite * 0.62);
t.recoverHealth?.(bite * 0.12); t.recoverHealth?.(bite * 0.12);
applyNeedShock(t, { safety: bite * 0.12 }); applyNeedShock(t, { safety: bite * 0.12 });
t.makePoop?.(bite * 0.08); t.makePoop?.(normalPoopUnitsFor("ant_corpse", bite));
return true; 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"); if (t.fightDisease) t.recoverFightDisease?.("\u305a\u3093\u3060\u9905\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
} }
const fromDuplicator = isDuplicatorFoodSource(target); 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) { if (fromDuplicator) {
t.mealCooldownUntil = Math.max(t.mealCooldownUntil || 0, (world.time || 0) + 1.2); t.mealCooldownUntil = Math.max(t.mealCooldownUntil || 0, (world.time || 0) + 1.2);
t.intentLockTimer = Math.max(0, Math.min(t.intentLockTimer || 0, 0.18)); 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.applyFoodHungerRelief ? t.applyFoodHungerRelief(bite * 0.46) : (t.hunger -= bite * 0.46);
t.recoverHealth?.(bite * 0.18); t.recoverHealth?.(bite * 0.18);
applyNeedRelief(t, { food: -2, fulfill: -1 }); applyNeedRelief(t, { food: -2, fulfill: -1 });
t.makePoop?.(bite * 0.10); t.makePoop?.(normalPoopUnitsFor("grass", bite));
return true; 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.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)); 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) }); 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 true;
} }
return false; return false;
@ -2109,7 +2125,7 @@ function updateMateBehavior(t, world, dt) {
function updateFightBehavior(t, world, dt) { function updateFightBehavior(t, world, dt) {
if (!t || !world) return false; if (!t || !world) return false;
if ((t.fightTimer || 0) > 0.04) { 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 }); 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; 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 === "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; 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; 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")); 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) }; 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 // \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 // \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 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; .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; if (crisis && crisis.needValue >= selectedEntry.needValue + 16) selectedEntry = crisis;
@ -2693,13 +2715,18 @@ function protectSleepSession(tarinai, world, needs) {
spriteId() { spriteId() {
const now = this.world?.time || 0; 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?.(); this.preloadSunbathSprites?.();
const sunbathId = this.sunbathSpriteId ? this.sunbathSpriteId() : (String(this.sunbathSpriteVariant || "").startsWith("sunbath") ? this.sunbathSpriteVariant : "sunbath_1"); const sunbathId = this.sunbathSpriteId ? this.sunbathSpriteId() : (String(this.sunbathSpriteVariant || "").startsWith("sunbath") ? this.sunbathSpriteVariant : "sunbath_1");
this.visibleSpriteId = sunbathId; this.visibleSpriteId = sunbathId;
this.spriteLockUntil = now + 0.55; this.spriteLockUntil = 0;
return sunbathId; return sunbathId;
} }
if (String(this.visibleSpriteId || "").startsWith("sunbath")) {
this.visibleSpriteId = "";
this.spriteLockUntil = 0;
}
const next = this.rawSpriteId(); const next = this.rawSpriteId();
const isCurrentIntimidator = this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04; const isCurrentIntimidator = this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04;
const isCurrentIntimidated = !isCurrentIntimidator && this.intimidatedTimer > 0.04; const isCurrentIntimidated = !isCurrentIntimidator && this.intimidatedTimer > 0.04;
@ -2753,19 +2780,50 @@ function protectSleepSession(tarinai, world, needs) {
return ""; 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"]; const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"];
if (!ids.length) return "sunbath_1"; if (!ids.length) return "sunbath_1";
const normalizedExclude = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(exclude) : String(exclude || ""); const normalizedExclude = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(exclude) : String(exclude || "");
const choices = ids.length > 1 ? ids.filter(id => id !== normalizedExclude) : ids; let candidates = ids;
return choices[Math.floor(Math.random() * choices.length)] || ids[0] || "sunbath_1"; 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() { sunbathSpriteId() {
const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"]; const ids = this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : ["sunbath_1", "sunbath_2", "sunbath_3"];
const current = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || ""); let current = this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || "");
if (!current || !ids.includes(current)) this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId() : "sunbath_1"; if (!current || !ids.includes(current)) {
return this.normalizeSunbathSpriteId ? this.normalizeSunbathSpriteId(this.sunbathSpriteVariant) : String(this.sunbathSpriteVariant || "sunbath_1"); 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() { isSunbathTime() {
@ -2784,9 +2842,11 @@ function protectSleepSession(tarinai, world, needs) {
if ((this.sunbathCooldown || 0) > 0) return false; if ((this.sunbathCooldown || 0) > 0) return false;
if (this.stuckPushpinId || this.currentLodgedPin?.()) 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 (["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; 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() { tryRecoverBySunbath() {
@ -2819,8 +2879,16 @@ function protectSleepSession(tarinai, world, needs) {
const pad = Math.max(28, CONFIG.worldPadding || 30); const pad = Math.max(28, CONFIG.worldPadding || 30);
const safeW = Math.max(pad * 2 + 1, Number(this.world?.w || 1000) || 1000); 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); 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); const startX = Number.isFinite(this.x) ? this.x : (Number.isFinite(this._lastValidX) ? this._lastValidX : pad);
this.y = clamp(Number.isFinite(this.y) ? this.y : safeH * 0.5, pad, safeH - 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; 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"; 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 }); 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.sunbathTimer = Math.max(6, (CONFIG.dayLength || 120) / 12);
this.sunbathCooldown = 0; this.sunbathCooldown = 0;
this.sunbathFrameTimer = 0.5; 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.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.spriteLockUntil = 0;
this.sunbathRecoveryChecked = false; this.sunbathRecoveryChecked = false;
this.vx = 0; this.vx = 0;
@ -2847,16 +2917,29 @@ function protectSleepSession(tarinai, world, needs) {
this.vx = 0; this.vx = 0;
this.vy = 0; this.vy = 0;
if ((this.sunbathFrameTimer || 0) <= 0) { if ((this.sunbathFrameTimer || 0) <= 0) {
const prev = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant; const prev = this._activeSunbathSpriteId || (this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant);
this.sunbathSpriteVariant = this.randomSunbathSpriteId ? this.randomSunbathSpriteId(prev) : `sunbath_${1 + Math.floor(Math.random() * 3)}`; const next = this.randomSunbathSpriteId ? this.randomSunbathSpriteId(prev, { readyOnly: true }) : `sunbath_${1 + Math.floor(Math.random() * 3)}`;
this.visibleSpriteId = this.sunbathSpriteId ? this.sunbathSpriteId() : this.sunbathSpriteVariant; if (next && next !== prev && (!this.isSunbathSpriteReady || this.isSunbathSpriteReady(next))) {
this.spriteLockUntil = (this.world?.time || 0) + 0.55; 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.sunbathFrameTimer = 0.5;
} }
this.energy = clamp((this.energy || 0) + dt * 0.03, 0, 100); 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.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") { if ((this.sunbathTimer || 0) <= 0 && this.state === "sunbath") {
this.finishSunbath?.(); this.finishSunbath?.();
this._activeSunbathSpriteId = "";
this.visibleSpriteId = "";
this.spriteLockUntil = 0;
this.sunbathAnchorX = null;
this.sunbathAnchorY = null;
this.goIdle("\u65e5\u5149\u6d74\u3092\u7d42\u3048\u305f"); this.goIdle("\u65e5\u5149\u6d74\u3092\u7d42\u3048\u305f");
} }
}, },
@ -2922,7 +3005,7 @@ function protectSleepSession(tarinai, world, needs) {
const beforeBirthTimer = this.birthRitualTimer; const beforeBirthTimer = this.birthRitualTimer;
this.birthRitualTimer = Math.max(0, this.birthRitualTimer - dt); this.birthRitualTimer = Math.max(0, this.birthRitualTimer - dt);
if (beforeBirthTimer > 0 && this.birthRitualTimer <= 0.04 && this.birthRitualLeader) { 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); if (partner) this.world.finishBirthRitual(this, partner);
this.birthRitualLeader = false; this.birthRitualLeader = false;
} }
@ -2952,7 +3035,7 @@ function protectSleepSession(tarinai, world, needs) {
this.fightCooldown = Math.max(0, this.fightCooldown - dt); this.fightCooldown = Math.max(0, this.fightCooldown - dt);
this.defeatedTimer = Math.max(0, this.defeatedTimer - dt); this.defeatedTimer = Math.max(0, this.defeatedTimer - dt);
if (wasFighting && this.fightTimer <= 0.04 && this.defeatedById) { 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.defeatedTimer = Math.max(this.defeatedTimer, rand(3.4, 5.2));
this.fearTimer = Math.max(this.fearTimer, 1.0 * this.personalityProfile().fear); this.fearTimer = Math.max(this.fearTimer, 1.0 * this.personalityProfile().fear);
this.hurtTimer = Math.max(this.hurtTimer, 1.8); 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.intentLockTimer = Math.max(0, (this.intentLockTimer || 0) - dt);
this.aiTimer += 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 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 aiJitter = stableUnit(this.familyKey || this.id, "ai-interval") * 0.35;
const pressure = clamp(Number(quality.pressure || 0) || 0, 0, 1); const aiInterval = urgentAi ? 0.35 : (1.0 + aiJitter);
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);
if (this.aiTimer >= aiInterval && this.world.spendScheduledWork?.("ai", urgentAi) !== false) { if (this.aiTimer >= aiInterval && this.world.spendScheduledWork?.("ai", urgentAi) !== false) {
const aiDt = Math.min(this.aiTimer, 0.48); const aiDt = Math.min(this.aiTimer, 0.48);
this.aiTimer = 0; this.aiTimer = 0;
@ -3047,7 +3127,7 @@ function protectSleepSession(tarinai, world, needs) {
this.updateFacing(); this.updateFacing();
} }
this.envTimer += dt; 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) { if (this.envTimer >= envInterval && this.world.spendScheduledWork?.("env", urgentAi || this.world.selected === this) !== false) {
const envDt = Math.min(this.envTimer, 1.2); const envDt = Math.min(this.envTimer, 1.2);
this.envTimer = 0; this.envTimer = 0;
@ -3099,10 +3179,11 @@ function protectSleepSession(tarinai, world, needs) {
} }
}, },
bubble(text, cooldown = 2.4, color = "rgba(42,36,29,0.78)") { bubble(text, cooldown = 2.4, color = "rgba(42,36,29,0.78)", options = {}) {
if (this.world.time < this.nextBubbleAt) return; if (!options.force && this.world.time < this.nextBubbleAt) return false;
this.nextBubbleAt = this.world.time + cooldown; this.nextBubbleAt = this.world.time + cooldown;
this.world.spawnBubble(this.x, this.y - this.radius * 1.35, text, color); this.world.spawnBubble(this.x, this.y - this.radius * 1.35, text, color);
return true;
}, },
mouthPosition(target = null) { mouthPosition(target = null) {
@ -3245,14 +3326,14 @@ function protectSleepSession(tarinai, world, needs) {
} }
if (this.birthRitualTimer > 0.04) { 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" }); this.setActionState?.("birth_ritual", { target: partner, reason: "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b" });
synchronizeActionTextFromState(this); synchronizeActionTextFromState(this);
return; return;
} }
if (this.intimidateTimer > 0.04) { 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" }); 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" }); 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); this.bubble("!", 1.2);
@ -3260,11 +3341,11 @@ function protectSleepSession(tarinai, world, needs) {
} }
if (this.fightTimer > 0.04) { 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; this.fightTargetId = this.fightTargetIds[0] || this.fightTargetId;
let best = null, bestD = Infinity; let best = null, bestD = Infinity;
for (const id of this.fightTargetIds) { 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; if (!t) continue;
const d = dist(this, t); const d = dist(this, t);
if (d < bestD) { best = t; bestD = d; } if (d < bestD) { best = t; bestD = d; }
@ -3277,7 +3358,7 @@ function protectSleepSession(tarinai, world, needs) {
} }
if (this.defeatedTimer > 0.04) { 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); applyNeedShock(this, { safety: 54, health: 16 }, opponent || null);
startNeedDrivenEmergencyReaction(this, opponent || null, "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u9003\u3052\u3066\u3044\u308b\u3002"); 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; if (!opponent && this.defeatedTimer < 0.12) this.defeatedById = null;
@ -3327,6 +3408,21 @@ function protectSleepSession(tarinai, world, needs) {
return; 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); const picked = choosePriorityNeedAction(needs, this, this.world);
if (!picked?.action) { if (!picked?.action) {
const sunbathAction = actionById("sunbath"); const sunbathAction = actionById("sunbath");
@ -3350,27 +3446,28 @@ function protectSleepSession(tarinai, world, needs) {
}, },
makePoop(force = 1) { makePoop(force = 1) {
this.digest += force; const add = Math.max(0, Number(force) || 0);
if (this.digest < 12.4) return; 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")) { if (this.hasLodgedPinEffect?.("blocksZunchi")) {
this.digest = rand(0.4, 1.6);
this.oshiriByoZunchiStock = Math.min(18, Math.max(0, this.oshiriByoZunchiStock || 0) + 1); 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"; 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 }); if ((this.oshiriByoZunchiStock || 0) >= 6) applyNeedShock(this, { health: 8 });
return; return false;
} }
this.digest = rand(0.4, 1.6); this.poopCount = (this.poopCount || 0) + 1;
this.poopCount += 1;
const side = this.facingDir(); const side = this.facingDir();
const backX = this.x - side * this.radius * rand(0.72, 0.96); const backX = this.x - side * this.radius * rand(0.72, 0.96);
const backY = this.y + this.radius * rand(0.25, 0.48); const backY = this.y + this.radius * rand(0.25, 0.48);
this.world.spawnZunchi(backX, backY, this); this.world.spawnZunchi(backX, backY, this);
const now = this.world?.time || 0; const now = this.world?.time || 0;
if (now >= (this.nextPoopBubbleAt || -999)) { this.nextPoopBubbleAt = now + 2.8;
this.nextPoopBubbleAt = now + 2.8; this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)", { force: true });
this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)");
}
if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] }); 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) { interactWithItems(dt) {
@ -3385,8 +3482,7 @@ function protectSleepSession(tarinai, world, needs) {
const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && mealReady && wantsFood && this.hunger > 18; 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 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 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 = 16;
const scanLimit = Math.max(3, Number(quality.itemScanLimit || 28) || 28);
const nearbyItems = this.world.nearbyItems(this.x, this.y, this.radius + 46); const nearbyItems = this.world.nearbyItems(this.x, this.y, this.radius + 46);
const foodCandidates = []; const foodCandidates = [];
if (this.target && !this.target.dead && (this.target.type === "duplicator" || roleMatches(this.target, "food") || roleMatches(this.target, "medicine"))) foodCandidates.push(this.target); 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"; this.lastMealColor = foodType === "sweet" ? "#78b957" : "#f1dfb7";
const fromDuplicator = isDuplicatorFoodSource(it); 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 (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.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; }
if (this.world.time - this.lastLog > 9 && Math.random() < 0.014) { 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")))); 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); 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 (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.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; }
if (this.world.time - this.lastLog > 9 && Math.random() < 0.02) { 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"); this.world.spawnEatEffect(mouth.x, mouth.y, "#5b9d39");
} }
if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.55; } 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; if (this.type === "jito") this.affection += dt * 0.25;
break; break;
} }
@ -3531,7 +3627,7 @@ function protectSleepSession(tarinai, world, needs) {
const mouth = this.mouthPosition(it); const mouth = this.mouthPosition(it);
this.world.spawnEatEffect(mouth.x, mouth.y, "#4a3f36"); this.world.spawnEatEffect(mouth.x, mouth.y, "#4a3f36");
this.lastMealColor = "#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; } if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.62; }
break; break;
} }

View file

@ -4,14 +4,145 @@
const Tarinai = global.Tarinai; const Tarinai = global.Tarinai;
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_render.js"); if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_render.js");
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ 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) { draw(ctx, t, lighting = null) {
if (this.dead) return; 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 lightState = lighting || getLightingState(this.world);
const quality = this.world.performanceQuality?.() || {}; if (sunbathActive && this.drawSunbathPose?.(ctx, t, lightState)) return;
const effectDensity = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1); if (!sunbathActive && String(this.visibleSpriteId || "").startsWith("sunbath")) {
const showDecor = effectDensity > 0.45 || this.world.selected === this; this.visibleSpriteId = "";
if (this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04) this.preloadSunbathSprites?.(); this.spriteLockUntil = 0;
const sprite = this.spriteId(); }
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 sunbathSprite = String(sprite || "").startsWith("sunbath");
const sunbathCandidates = sunbathSprite && this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : []; const sunbathCandidates = sunbathSprite && this.sunbathSpriteCandidates ? this.sunbathSpriteCandidates() : [];
if (sunbathSprite) { if (sunbathSprite) {
@ -20,18 +151,28 @@
else if (globalThis.TarinaiAssets?.ensureImage) globalThis.TarinaiAssets.ensureImage(id); else if (globalThis.TarinaiAssets?.ensureImage) globalThis.TarinaiAssets.ensureImage(id);
} }
} }
const spriteFallback = sunbathSprite ? "" : "smile"; const spriteFallback = "smile";
let renderSprite = sprite; let renderSprite = sprite;
let img = typeof getRenderableImage === "function" ? getRenderableImage(sprite, spriteFallback) : (images.get(sprite) || images.get(spriteFallback) || images.get(this.type)); let img = typeof getRenderableImage === "function" ? getRenderableImage(sprite, spriteFallback) : (images.get(sprite) || images.get(spriteFallback) || images.get(this.type));
if (sunbathSprite) { if (sunbathSprite) {
const ready = (id) => typeof isImageReady === "function" ? isImageReady(id) : !!(images?.get?.(id)?.complete || images?.get?.(id)?.naturalWidth || images?.get?.(id)?.width); const ready = (id) => typeof isImageReady === "function" ? isImageReady(id) : !!(images?.get?.(id)?.complete || images?.get?.(id)?.naturalWidth || images?.get?.(id)?.width);
if (!ready(sprite)) { const imageFor = (id) => (typeof images !== "undefined" ? images.get?.(id) : globalThis.TarinaiAssets?.images?.get?.(id)) || null;
const fallbackSunbath = sunbathCandidates.find(id => ready(id)); if (ready(sprite)) {
this._lastRenderableSunbathSpriteId = sprite;
} else {
const orderedFallbacks = [this._lastRenderableSunbathSpriteId, ...sunbathCandidates].filter(Boolean);
const fallbackSunbath = orderedFallbacks.find(id => ready(id));
if (fallbackSunbath) { if (fallbackSunbath) {
renderSprite = 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)); 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"); const metrics = getImageMetrics(renderSprite) || getImageMetrics(sprite) || getImageMetrics(this.type) || getImageMetrics("smile");
@ -131,7 +272,7 @@
ctx.restore(); ctx.restore();
} }
if (this.birthRitualTimer > 0.04 && this.birthRitualLeader) { 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 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 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; 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 cacheW = Math.max(1, Math.round((dw * renderScale) / 8) * 8);
const cacheH = Math.max(1, Math.round((dh * renderScale) / 8) * 8); const cacheH = Math.max(1, Math.round((dh * renderScale) / 8) * 8);
if (img) { if (img) {
let spriteCacheKey = sprite; let spriteCacheKey = renderSprite || sprite;
try { try {
if (typeof images !== "undefined" && images.get?.(sprite) !== img) { if (typeof images !== "undefined" && images.get?.(renderSprite || sprite) !== img) {
let fallbackId = "fallback"; let fallbackId = "fallback";
for (const [imageId, imageObj] of images) { for (const [imageId, imageObj] of images) {
if (imageObj === img) { fallbackId = imageId; break; } if (imageObj === img) { fallbackId = imageId; break; }
} }
spriteCacheKey = `${sprite}:fallback:${fallbackId}`; spriteCacheKey = `${renderSprite || sprite}:fallback:${fallbackId}`;
} }
} catch (err) { } 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); ctx.drawImage(spriteCanvas, -dw / 2, -dh / 2, dw, dh);
} else { } else {
ctx.save(); 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) { contains(x, y) {
if (this.insideNestBoxId || (this.nestFade || 0) >= 0.92) return false; 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; const hit = this.world.screenSizeToWorld ? this.world.screenSizeToWorld(this.radius * 1.9) : this.radius * 1.9;

View file

@ -6,8 +6,7 @@
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
interactWithOthers(dt) { interactWithOthers(dt) {
let closeCount = 0; let closeCount = 0;
const quality = this.world?.performanceQuality?.() || {}; const scanLimit = 8;
const scanLimit = Math.max(1, Number(quality.socialScanLimit || 14) || 14);
let scanned = 0; let scanned = 0;
for (const o of this.world.nearbyTarinai(this.x, this.y, 78)) { for (const o of this.world.nearbyTarinai(this.x, this.y, 78)) {
if (o === this || o.dead) continue; if (o === this || o.dead) continue;
@ -191,13 +190,23 @@
if (this.state === "sunbath") { if (this.state === "sunbath") {
this.target = null; this.target = null;
this.vx *= Math.pow(0.90, dt * 60); this.vx = 0;
this.vy *= Math.pow(0.90, dt * 60); this.vy = 0;
const pad = Math.max(28, CONFIG.worldPadding || 30); const pad = Math.max(28, CONFIG.worldPadding || 30);
const safeW = Math.max(pad * 2 + 1, Number(this.world?.w || 1000) || 1000); 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); 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); const fallbackX = Number.isFinite(this._lastSunbathDrawX) ? this._lastSunbathDrawX : (Number.isFinite(this._lastValidX) ? this._lastValidX : (Number.isFinite(this.x) ? this.x : 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 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; return;
} }
if (this.state === "sleep") { if (this.state === "sleep") {
@ -234,7 +243,7 @@
} }
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") { 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) { if (partner) {
const midX = (this.x + partner.x) / 2; const midX = (this.x + partner.x) / 2;
const midY = (this.y + partner.y) / 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)); 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) { if (Math.random() < 0.55) {
const spot = this.world.findGrassPlantingSpot(this.x, this.y, { allowOriginal: false, minRadius: 26, maxRadius: 76 }); 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.world.markDead(this, finalReason);
this.releasedLiveToken = this.liveToken || 0; this.releasedLiveToken = this.liveToken || 0;

View file

@ -2,10 +2,7 @@
function resizeCanvas() { function resizeCanvas() {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const quality = world?.performanceQuality?.() || {}; const dpr = Math.max(0.6, Math.min(window.devicePixelRatio || 1, 2));
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 nextW = Math.max(1, Math.floor(rect.width * dpr)); const nextW = Math.max(1, Math.floor(rect.width * dpr));
const nextH = Math.max(1, Math.floor(rect.height * dpr)); const nextH = Math.max(1, Math.floor(rect.height * dpr));
if (canvas.width === nextW && canvas.height === nextH && uiCache.canvasDpr === dpr) return; if (canvas.width === nextW && canvas.height === nextH && uiCache.canvasDpr === dpr) return;

View file

@ -1,8 +1,8 @@
"use strict"; "use strict";
(function () { (function () {
const APP_VERSION = "15.24.53"; const APP_VERSION = "16.00.01";
const APP_BUILD = "tool-ui-icon-scale-collapse-fix"; const APP_BUILD = "lag-pass-static-schedule";
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;

View file

@ -20,7 +20,6 @@
shouldDropRainWater(worldRef, dt = 0) { shouldDropRainWater(worldRef, dt = 0) {
return Boolean( return Boolean(
worldRef?.weather === "light_rain" && worldRef?.weather === "light_rain" &&
(worldRef.itemCounts?.water || 0) < 24 &&
Math.random() < Math.max(0, Number(dt) || 0) * 0.12 Math.random() < Math.max(0, Number(dt) || 0) * 0.12
); );
}, },

View file

@ -6,35 +6,40 @@
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
updateAnts(dt) { updateAnts(dt) {
if (!this.ants) this.ants = []; if (!this.ants) this.ants = [];
const nests = this.itemsOfType?.("ant_nest") || (this.items || []).filter(it => it && !it.dead && it.type === "ant_nest"); this.antNestUpdateAccum = (this.antNestUpdateAccum || 0) + dt;
for (const nest of nests) { if (this.antNestUpdateAccum >= 3.0) {
if (typeof ensureAntNestWorkerPool === "function") ensureAntNestWorkerPool(nest); const nestDt = this.antNestUpdateAccum;
if (!Number.isFinite(nest.antCount)) nest.antCount = ANT_NEST_START_COUNT || 6; this.antNestUpdateAccum = 0;
if (typeof syncAntNestCount === "function") syncAntNestCount(this, nest); const nests = this.itemsOfType?.("ant_nest") || (this.items || []).filter(it => it && !it.dead && it.type === "ant_nest");
nest.antSpawnTimer = Math.max(0, (nest.antSpawnTimer || 0) - dt); for (const nest of nests) {
const outside = this.ants.filter(a => a && !a.dead && a.kind === "worker" && a.homeId === nest.id).length; if (typeof ensureAntNestWorkerPool === "function") ensureAntNestWorkerPool(nest);
const available = Array.isArray(nest.antWorkers) ? nest.antWorkers.length : Math.max(0, (nest.antCount || 0) - outside); if (!Number.isFinite(nest.antCount)) nest.antCount = ANT_NEST_START_COUNT || 6;
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); if (typeof syncAntNestCount === "function") syncAntNestCount(this, nest);
nest.antSpawnTimer = rand(1.6, 3.8) + outside * 0.45; nest.antSpawnTimer = Math.max(0, (nest.antSpawnTimer || 0) - nestDt);
this.drawListDirty = true; 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 aliveAntCount = (this.ants || []).reduce((n, a) => n + (a && !a.dead ? 1 : 0), 0);
const q = this.performanceQuality ? this.performanceQuality() : null; const stride = aliveAntCount >= 28 ? 3 : 2;
const stride = Math.max(1, Math.min(3, q?.antStride || (aliveAntCount >= 70 ? 3 : aliveAntCount >= 28 ? 2 : 1)));
this.antUpdatePhase = ((this.antUpdatePhase || 0) + 1) % stride; this.antUpdatePhase = ((this.antUpdatePhase || 0) + 1) % stride;
for (let i = 0; i < this.ants.length; i++) { for (let i = 0; i < this.ants.length; i++) {
const a = this.ants[i]; const a = this.ants[i];

View file

@ -288,15 +288,6 @@
}, },
spawnZunchi(x, y, source = null) { 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)); const it = new Item("zunchi", clamp(x, 40, this.w - 40), clamp(y, 40, this.h - 40));
it.amount = 260; it.amount = 260;
it.stage = "fresh"; it.stage = "fresh";

View file

@ -63,7 +63,11 @@
generation: 1, 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.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.updateItemCounts();
this.updateEffectCounts(); this.updateEffectCounts();

View file

@ -365,17 +365,10 @@
placeItem(item, dropped = true) { placeItem(item, dropped = true) {
if (!item) return null; if (!item) return null;
if (item.type === "grass" && (this.itemCounts?.grass || 0) >= (this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 96))) { if (item.type === "grass") {
showToast("\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u306e\u8349\u306e\u4e0a\u9650\u306b\u9054\u3057\u3066\u3044\u307e\u3059\u3002"); item.grassLimitExempt = true;
return null; item.manualGrass = true;
} item.placedByPlayer = true;
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 (this.placementBlocked(item)) { 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"); 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.dropTimer = item.dropMax;
item.dropImpactDone = false; 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:place", { item, type: item.type, dropped });
this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type }); this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type });
if (dropped) audio.place?.(item.type); if (dropped) audio.place?.(item.type);

View file

@ -12,25 +12,17 @@
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
performanceLevel() { 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; return 0;
}, },
performanceQuality() { 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?.()) { beginFramePerformanceBudgets() {
const pressure = clamp(Number(quality?.pressure || 0) || 0, 0, 1);
const density = clamp(Number(quality?.effectDensity || (1 - pressure)) || 0, 0.12, 1);
this.workBudget = { this.workBudget = {
ai: Math.max(4, Math.round(4 + density * 24)), ai: Math.max(1, Math.ceil(((this.tarinai || []).length || 1) / 2)),
env: Math.max(3, Math.round(3 + density * 15)), env: Math.max(1, Math.ceil(((this.tarinai || []).length || 1) / 3)),
aiUsed: 0, aiUsed: 0,
envUsed: 0, envUsed: 0,
}; };
@ -106,11 +98,11 @@
}, },
performance: { performance: {
liveTarinai, liveTarinai,
fps: window.TarinaiPerformance?.fps || window.__tarinaiFps || 0, fps: window.__tarinaiFps || window.TarinaiPerformance?.fps || 0,
pressure: this.performanceQuality?.().pressure || 0, pressure: 0,
level: this.performanceLevel?.() || 0, level: 0,
dpr: (typeof uiCache !== "undefined" ? uiCache?.canvasDpr : window.uiCache?.canvasDpr) || 0, dpr: (typeof uiCache !== "undefined" ? uiCache?.canvasDpr : window.uiCache?.canvasDpr) || 0,
quality: this.performanceQuality?.() || null, quality: null,
}, },
work: { work: {
budget: this.workBudget || null, budget: this.workBudget || null,
@ -121,10 +113,9 @@
grassLoadLevel() { grassLoadLevel() {
const grass = this.itemCounts?.grass || 0; const grass = this.itemCounts?.grass || 0;
const perf = this.performanceLevel(); if (grass >= 180) return 3;
if (perf >= 3 || grass >= 180) return 3; if (grass >= 120) return 2;
if (perf >= 2 || grass >= 120) return 2; if (grass >= 72) return 1;
if (perf >= 1 || grass >= 72) return 1;
return 0; return 0;
}, },
@ -132,6 +123,29 @@
return [1.0, 1.35, 1.85, 2.45][this.grassLoadLevel()] || 1.0; 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() { sortedDrawList() {
if (!this.drawList) this.drawList = []; if (!this.drawList) this.drawList = [];
const antCount = liveCount(this.ants); const antCount = liveCount(this.ants);
@ -149,6 +163,10 @@
ensureSpatial(reason = "read") { ensureSpatial(reason = "read") {
if (!this.spatial) return false; if (!this.spatial) return false;
if (!this.spatialDirty && (this.spatialVersion || 0) > 0) 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); return this.rebuildSpatial(true, reason);
}, },
@ -213,6 +231,9 @@
addItem(item, reason = "add-item", opts = {}) { addItem(item, reason = "add-item", opts = {}) {
if (!item) return null; 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.items.push(item);
this.markItemBucketsDirty?.(reason); this.markItemBucketsDirty?.(reason);
this.markSpatialDirty?.(reason); this.markSpatialDirty?.(reason);
@ -273,7 +294,6 @@
}, },
compactItems() { compactItems() {
this.enforceItemBudget();
const before = this.items.length; const before = this.items.length;
let write = 0; let write = 0;
for (let read = 0; read < this.items.length; read++) { for (let read = 0; read < this.items.length; read++) {
@ -291,48 +311,8 @@
return changed; 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() { compactEffects() {
const limit = this.performanceQuality?.().effectLimit ?? CONFIG.effectLimit ?? 96; const limit = CONFIG.effectLimit ?? 96;
if (Number.isFinite(limit) && this.effects.length > limit) { 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))); 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; const remove = this.effects.length - limit;

View file

@ -3,6 +3,46 @@
(function (global) { (function (global) {
const World = global.World; const World = global.World;
if (!World) throw new Error("World is not available for mixin: world_update.js"); 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) { function markSpatialDirtyIfMoved(worldRef, list, reason = "moved", threshold = 0.35) {
if (!worldRef || !Array.isArray(list) || !list.length) return false; if (!worldRef || !Array.isArray(list) || !list.length) return false;
const thresholdSq = threshold * threshold; const thresholdSq = threshold * threshold;
@ -30,7 +70,9 @@
return moved; return moved;
} }
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ 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 alive = this.tarinai || [];
const avgStress = alive.length ? alive.reduce((sum, t) => sum + (t.stress || 0), 0) / alive.length : 0; const avgStress = alive.length ? alive.reduce((sum, t) => sum + (t.stress || 0), 0) / alive.length : 0;
let fightPressure = 0; let fightPressure = 0;
@ -52,7 +94,9 @@
const foodSpoilage = this.foodSpoilagePenalty || 0; 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 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); 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 (d < bestD && d <= Math.max(60, (it.r || 24) * 2.0)) { best = it; bestD = d; }
} }
if (!best) return null; 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 }; return { bed: best, owner };
}, },
@ -253,11 +297,9 @@
this.spatialDirtyMarksThisFrame = 0; this.spatialDirtyMarksThisFrame = 0;
this.ensureSpatial?.("update-start"); this.ensureSpatial?.("update-start");
const quality = this.performanceQuality?.() || {}; this.beginFramePerformanceBudgets?.();
const perf = Number(quality.level ?? this.performanceLevel?.() ?? 0) || 0;
this.beginFramePerformanceBudgets?.(quality);
const itemCountBefore = this.items.length; 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); const itemsMoved = markSpatialDirtyIfMoved(this, this.items, "items-moved", 0.25);
if (this.itemCounts?.ball) { if (this.itemCounts?.ball) {
this.ensureSpatial?.("ball-ball-collisions"); this.ensureSpatial?.("ball-ball-collisions");
@ -274,7 +316,7 @@
t.update(dt); t.update(dt);
} }
const now = this.time || 0; const now = this.time || 0;
const bedConflictInterval = Number(quality.bedConflictInterval || 0.12) || 0.12; const bedConflictInterval = 3.2;
if (now >= (this.nextBedConflictCheckAt || 0)) { if (now >= (this.nextBedConflictCheckAt || 0)) {
this.nextBedConflictCheckAt = now + bedConflictInterval; this.nextBedConflictCheckAt = now + bedConflictInterval;
this.workStats && (this.workStats.bedConflictRuns = (this.workStats.bedConflictRuns || 0) + 1); this.workStats && (this.workStats.bedConflictRuns = (this.workStats.bedConflictRuns || 0) + 1);
@ -286,7 +328,7 @@
this.ensureSpatial?.("post-tarinai-update"); this.ensureSpatial?.("post-tarinai-update");
this.limitBallChasers(); this.limitBallChasers();
this.resolveBallInteractions(dt); this.resolveBallInteractions(dt);
const collisionInterval = Number(quality.collisionInterval || 0.04) || 0.04; const collisionInterval = 1.8;
if (now >= (this.nextTarinaiCollisionCheckAt || 0)) { if (now >= (this.nextTarinaiCollisionCheckAt || 0)) {
this.nextTarinaiCollisionCheckAt = now + collisionInterval; this.nextTarinaiCollisionCheckAt = now + collisionInterval;
this.workStats && (this.workStats.collisionRuns = (this.workStats.collisionRuns || 0) + 1); this.workStats && (this.workStats.collisionRuns = (this.workStats.collisionRuns || 0) + 1);
@ -296,7 +338,7 @@
} }
this.compactTimer += dt; this.compactTimer += dt;
const compactInterval = perf >= 2 ? 0.95 : 0.58; const compactInterval = 2.5;
let itemsCompacted = false; let itemsCompacted = false;
if (this.compactTimer >= compactInterval) { if (this.compactTimer >= compactInterval) {
itemsCompacted = this.compactItems(); itemsCompacted = this.compactItems();
@ -307,20 +349,20 @@
} }
this.countsTimer += dt; this.countsTimer += dt;
const countsInterval = perf >= 2 ? 0.85 : 0.42; const countsInterval = 2.0;
if (this.countsTimer >= countsInterval || itemsCompacted || this.items.length !== itemCountBefore) { if (this.countsTimer >= countsInterval || itemsCompacted || this.items.length !== itemCountBefore) {
this.updateItemCounts(); this.updateItemCounts();
this.updateEffectCounts(); this.updateEffectCounts();
this.countsTimer = 0; this.countsTimer = 0;
} }
if ((this.outOfBoundsCheckAt || -999) + 1.0 <= this.time) { if ((this.outOfBoundsCheckAt || -999) + 5.0 <= this.time) {
this.outOfBoundsCheckAt = this.time; this.outOfBoundsCheckAt = this.time;
this.sanitizeOutOfBounds(); this.sanitizeOutOfBounds();
} }
this.drawSortTimer += dt; this.drawSortTimer += dt;
const sortInterval = perf >= 2 ? 0.16 : perf === 1 ? 0.11 : 0.07; const sortInterval = 0.25;
if (this.drawSortTimer >= sortInterval) { if (this.drawSortTimer >= sortInterval) {
this.drawListDirty = true; this.drawListDirty = true;
this.drawSortTimer = 0; this.drawSortTimer = 0;
@ -343,8 +385,7 @@
} }
const grassCount = this.itemCounts.grass || 0; if (Math.random() < dt * 0.109) {
if (grassCount < Math.min(108, this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 360)) && Math.random() < dt * 0.109) {
const spot = this.findGrassPlantingSpot(rand(60, this.w - 60), rand(60, this.h - 60), { const spot = this.findGrassPlantingSpot(rand(60, this.w - 60), rand(60, this.h - 60), {
allowOriginal: true, allowOriginal: true,
minRadius: 28, minRadius: 28,
@ -355,7 +396,8 @@
const sprout = new Item("grass", spot.x, spot.y); const sprout = new Item("grass", spot.x, spot.y);
sprout.amount = rand(30, 62); sprout.amount = rand(30, 62);
sprout.growth = rand(0.18, 0.38); 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); const far = Math.max(360, Math.max(this.w || 1000, this.h || 720) * 0.75);
let changedItems = false; let changedItems = false;
const repairPoint = (obj, radius = 12) => { const repairPoint = (obj, radius = 12) => {
if (!obj) return "delete"; if (!obj) return "missing";
if (!Number.isFinite(obj.x) || !Number.isFinite(obj.y)) return "delete"; 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 minX = pad - radius;
const maxX = (this.w || 1000) - pad + radius; const maxX = safeW - pad + radius;
const minY = 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 >= 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, safeW - pad);
obj.x = clamp(obj.x, pad, (this.w || 1000) - pad); obj.y = clamp(obj.y, pad, safeH - pad);
obj.y = clamp(obj.y, pad, (this.h || 720) - pad);
if (Number.isFinite(obj.vx)) obj.vx *= -0.18; if (Number.isFinite(obj.vx)) obj.vx *= -0.18;
if (Number.isFinite(obj.vy)) obj.vy *= -0.18; if (Number.isFinite(obj.vy)) obj.vy *= -0.18;
return "moved"; return "moved";
}; };
for (const it of this.items || []) { for (const it of this.items || []) {
const result = repairPoint(it, it.r || 12); const result = repairPoint(it, it.r || 12);
if (result === "delete") { it.amount = 0; changedItems = true; } if (result === "moved") changedItems = true;
else if (result === "moved") changedItems = true;
} }
for (const ef of this.effects || []) { for (const ef of this.effects || []) {
const result = repairPoint(ef, ef.size || 12); repairPoint(ef, ef.size || 12);
if (result === "delete") ef.life = 0;
} }
const safeW = Math.max(pad * 2 + 1, Number(this.w || 1000) || 1000); const safeW = Math.max(pad * 2 + 1, Number(this.w || 1000) || 1000);
const safeH = Math.max(pad * 2 + 1, Number(this.h || 720) || 720); const safeH = Math.max(pad * 2 + 1, Number(this.h || 720) || 720);
@ -412,18 +454,34 @@
}; };
for (const t of this.tarinai || []) { for (const t of this.tarinai || []) {
if (!t || t.dead) continue; 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); 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; let changedAnts = false;
for (const ant of this.ants || []) { for (const ant of this.ants || []) {
if (!ant || ant.dead) continue; if (!ant || ant.dead) continue;
const result = repairPoint(ant, ant.kind === "queen" ? 18 : 10); const result = repairPoint(ant, ant.kind === "queen" ? 18 : 10);
if (result === "delete") { if (result === "missing") continue;
ant.dead = true;
changedAnts = true;
continue;
}
if (result === "moved") { if (result === "moved") {
ant.vx = 0; ant.vx = 0;
ant.vy = 0; ant.vy = 0;

View file

@ -67,8 +67,7 @@
this.markTerrainDirty("terrain-signature"); this.markTerrainDirty("terrain-signature");
return; return;
} }
const q = window.TarinaiPerformance?.quality?.(this) || {}; const interval = 3.0;
const interval = q.terrainRedrawInterval || (this.performanceLevel() >= 2 ? 1.05 : 0.62);
if ((this.time || 0) - (this.terrainLastDirtyAt || 0) >= interval) this.markTerrainDirty("terrain-periodic"); if ((this.time || 0) - (this.terrainLastDirtyAt || 0) >= interval) this.markTerrainDirty("terrain-periodic");
}, },
@ -98,23 +97,6 @@
return Math.max(0.22, field.worldScale || 1); 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 = "") { isFenceType(type = "") {
return type === "fence_v" || type === "fence_h"; return type === "fence_v" || type === "fence_h";
}, },

View file

@ -1,6 +1,6 @@
"use strict"; "use strict";
const APP_VERSION = "15.24.53"; const APP_VERSION = "16.00.01";
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const v = `v=${APP_VERSION}`; const v = `v=${APP_VERSION}`;
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files. // Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.