From acf93ec6df5f8a839a2c23595e91bad6d429bb62 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Wed, 1 Jul 2026 22:59:49 +0900 Subject: [PATCH] 1.5 --- AI_MAP.md | 6 +- GAME_FEATURES.md | 8 +- app_manifest.json | 2 +- index.html | 328 ++++++++++++++-------------- js/constraint_system.js | 9 +- js/fire_runtime_system.js | 7 +- js/health.js | 15 +- js/item_dynamic_tool_system.js | 2 - js/item_update_scheduler.js | 16 +- js/main.js | 2 +- js/perf_profiler.js | 30 +-- js/render.js | 28 ++- js/simulation_creature_system.js | 3 +- js/simulation_effects_system.js | 39 +++- js/simulation_environment_system.js | 6 +- js/simulation_maintenance_system.js | 16 +- js/tarinai_item_targeting.js | 2 +- js/tarinai_update_policy.js | 21 +- js/version.js | 4 +- js/world_reset_presets.js | 34 ++- js/world_spatial_budget.js | 1 + service-worker.js | 2 +- 22 files changed, 320 insertions(+), 261 deletions(-) diff --git a/AI_MAP.md b/AI_MAP.md index 6c04a96..f801193 100644 --- a/AI_MAP.md +++ b/AI_MAP.md @@ -8,6 +8,7 @@ Purpose: static Japanese browser simulation/game. Optimize context by reading th - Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader. - Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.103`. - Main loop: `js/main.js` initializes assets/audio/world/UI/save, then drives update/render. +- Performance/display settings: `js/perf_profiler.js` owns manual visual settings and profiler buckets; display tiers affect rendering/visuals, not simulation-quality branches. - Update order: `js/system_order.js` calls phase facades from `js/simulation_systems.js`. - Command path: UI/input emits command objects; `js/command_dispatcher.js` routes tool, pointer, selection, save, reset, and UI intents. - Persistence: save files are schema/codec/storage/coordinator modules; snapshot/history modules keep world restoration and stats consistent. @@ -45,6 +46,8 @@ Purpose: static Japanese browser simulation/game. Optimize context by reading th - Tool behavior: `js/command_dispatcher.js`, `js/world_tool_actions.js`, `js/item_registry.js`, relevant `js/item_*` or `js/tarinai_item_*`. - Creature behavior: `js/tarinai.js`, `js/tarinai_update_pipeline.js`, relevant `js/tarinai_*`, `js/system_order.js`. - World/environment/combat: matching `js/world_*`, `js/simulation_*`, `js/system_order.js`. +- Fire/burning behavior: `js/fire_runtime_system.js`, `js/item_dynamic_tool_system.js`, relevant item definitions, `js/world_combat_effects.js`, and nearby environment helpers. +- Performance/crowding: `js/perf_profiler.js`, `js/item_update_scheduler.js`, `js/tarinai_update_policy.js`, `js/constraint_system.js`, `js/world_spatial_budget.js`, and render/detail code. - Save/load: `js/save_schema.js`, `js/save_codec.js`, `js/save_system.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`. - Family tree: `js/world_family_social.js`, `js/family_graph.js`, `js/ui_family_*.js`. - Static app/cache/version: `app_manifest.json`, `scripts/generate_app_files.py`, `service-worker.js`, `js/version.js`, `index.html`. @@ -53,6 +56,7 @@ Purpose: static Japanese browser simulation/game. Optimize context by reading th - Preserve `index.html` script order unless all dependent manifests/generated files are updated together. - Keep `app_manifest.json`, `js/version.js`, `service-worker.js`, and cache query versions synchronized. -- Keep global exports for compatibility with regression checks and browser runtime. +- Keep intentional global exports for compatibility with regression checks and browser runtime; remove public facades only after repo-wide caller checks. - Prefer focused edits in the owning subsystem; split modules are intentional compatibility boundaries. +- Avoid reintroducing `simulationQualityTier` branches unless a real simulation-quality setting is restored; current quality controls are visual/detail controls. - Run `node --check js/*.js` and `python scripts/regression_check.py` for behavior changes. diff --git a/GAME_FEATURES.md b/GAME_FEATURES.md index 8a77557..cdfd96c 100644 --- a/GAME_FEATURES.md +++ b/GAME_FEATURES.md @@ -17,7 +17,7 @@ The core loop is: - **Canvas / field**: the main simulation area. Tarinai, items, effects, ants, weather, and terrain are drawn here. - **Top buttons**: pause/resume, speed control, ecology guide, sound settings, credits, save, display settings, and reset. -- **Display settings**: manually adjust effects, shadows, and drawing detail. `High` keeps quality-preserving optimizations active, while lower tiers cut heavier visuals for crowded scenes without lowering object limits. +- **Display settings**: manually adjust effects, shadows, and drawing detail. `High` keeps quality-preserving optimizations active, while lower tiers cut heavier visuals for crowded scenes without lowering object limits; simulation cadence remains quality-preserving. - **Mobile mode controls**: auto, camera, and tool modes for touch screens. - **Right panel tabs**: jump between main game, selected tarinai data, tools, colony, events, and family tree. - **Tarinai data panel**: appears when a tarinai is selected. Shows health, needs, personality, active effects, relations, inventory, behavior reason, and records. @@ -48,7 +48,7 @@ The core loop is: - **ずんだ餅 / sweet**: favored food; can cure some illness. - **へこ餅 / love mochi**: makes reproduction behavior more likely for a while. - **けんか餅 / fight mochi**: makes fighting behavior more likely for a while. -- **草 / grass**: food and comfort source; can grow and be used for simple beds. +- **草 / grass**: food and comfort source; can grow, be used for simple beds, and sometimes dampen nearby fire instead of immediately catching. - **水滴 / water**: drinkable water, also appears during rain and can be set in duplicators. - **ずんだ汁 / zunda juice**: clears item effects from a tarinai. - **複製機 / duplicator**: can be loaded with compatible food, medicine, grass, water, pushpins, or oshibyo and then produces or applies that content. @@ -89,7 +89,7 @@ The core loop is: - **石 / stone**: heavy object; dangerous when dropped or moved into others. - **げんこつ / genkotsu**: large fist impact on the ground. -- **爆竹 / firecracker**: creates an explosion. +- **爆竹 / firecracker**: creates an explosion; flame variants scatter fire while respecting active-fire limits. - **画鋲 / pushpin**: rolls when dropped; can lodge in tarinai and cause panic plus continuous damage. - **おしり鋲 / oshibyo**: blocks zunchi when lodged; pulling it out releases stored zunchi. - **射撃 / shoot**: attacks with a gun effect. @@ -140,7 +140,7 @@ Tarinai can be pushed by physics objects, carried or affected by ants, dragged b - **Field types**: park, garden, and cage provide different field sizes. - **Ground types**: sand, dirt, concrete, blanket, foot massage, ice, and laboratory-style ground affect stress, grass, and movement. - **Weather**: rain and other weather influence water, mood, and environment effects. -- **Grass ecology**: grass grows, feeds tarinai, comforts them, and can become bed material. +- **Grass ecology**: grass grows, feeds tarinai, comforts them, can become bed material, and has a small chance to reduce fire before burning. - **Dirt and corpses**: zunchi, splats, corpses, and ant corpses affect hygiene, disease, and observation results. - **Day/light conditions**: lighting and sun influence rendering and sunbath behavior. diff --git a/app_manifest.json b/app_manifest.json index c7f4c56..ef67904 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "39.15.107", + "version": "39.15.110", "css": [ "css/base.css", "css/layout.css", diff --git a/index.html b/index.html index fb2185f..edad51f 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,11 @@ - - - - - + + + + +
@@ -257,164 +257,164 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/constraint_system.js b/js/constraint_system.js index 91ee96c..2856ca1 100644 --- a/js/constraint_system.js +++ b/js/constraint_system.js @@ -483,17 +483,15 @@ } function ropeLodSettings(worldRef, length) { - const tier = global.TarinaiPerf.simulationQualityTier?.() || "high"; const ropeCount = Number(worldRef?.itemCounts?.rope || 0) || 0; let spacing = 34; let maxParticles = 24; let iterations = 6; let particleStride = 1; - if (tier === "low") { spacing = 56; maxParticles = 15; iterations = 4; particleStride = 2; } if (ropeCount > 12) { spacing *= 1.18; maxParticles = Math.max(10, maxParticles - 4); iterations = Math.max(3, iterations - 1); } if (ropeCount > 24) { spacing *= 1.28; maxParticles = Math.max(8, maxParticles - 4); iterations = Math.max(3, iterations - 1); particleStride = 2; } const targetCount = Math.max(4, Math.min(maxParticles, Math.ceil(Math.max(24, length) / spacing) + 1)); - return { tier, ropeCount, spacing, maxParticles, iterations, particleStride, targetCount }; + return { ropeCount, spacing, maxParticles, iterations, particleStride, targetCount }; } function ensureRopeParticles(item, a, b, length, worldRef = null) { @@ -681,8 +679,7 @@ const getRopeObstacleRects = () => { if (ropeObstacleRects) return ropeObstacleRects; if (!worldRef?.nearbySolidObstacleRects) return (ropeObstacleRects = []); - const maxChecks = lod.tier === "low" ? 18 : 38; - ropeObstacleRects = worldRef.nearbySolidObstacleRects(ropeCx, ropeCy, ropeQueryRadius + 64, { maxChecks }) || []; + ropeObstacleRects = worldRef.nearbySolidObstacleRects(ropeCx, ropeCy, ropeQueryRadius + 64, { maxChecks: 38 }) || []; return ropeObstacleRects; }; const collideParticles = () => { @@ -714,7 +711,7 @@ for (let iter = 0; iter < lod.iterations; iter += 1) { ps[0].x = a.x; ps[0].y = a.y; ps[ps.length - 1].x = b.x; ps[ps.length - 1].y = b.y; - const shouldCollide = lod.tier === "low" ? iter === 1 : (iter === 2 || iter === lod.iterations - 1); + const shouldCollide = iter === 2 || iter === lod.iterations - 1; if (shouldCollide) changed = collideParticles() || changed; for (let i = 0; i < ps.length - 1; i += 1) { const p1 = ps[i], p2 = ps[i + 1]; diff --git a/js/fire_runtime_system.js b/js/fire_runtime_system.js index 1e88fd4..461a019 100644 --- a/js/fire_runtime_system.js +++ b/js/fire_runtime_system.js @@ -212,8 +212,9 @@ function updateFirecracker(item, dt, worldRef) { for (const it of nearby) { if (!it || it.dead || it === item) continue; if (it.type === "grass" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 16, 12)) { - changed = tryGrassExtinguishFire(it, item, worldRef, dt) || changed; - if ((item.amount || 0) > 0) changed = igniteGrassByFire(it, item, worldRef, dt) || changed; + const extinguished = tryGrassExtinguishFire(it, item, worldRef, dt); + changed = extinguished || changed; + if (!extinguished && (item.amount || 0) > 0) changed = igniteGrassByFire(it, item, worldRef, dt) || changed; } if (it.type === "zunchi" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 15, 12)) changed = igniteZunchiByFire(it, item, worldRef, dt) || changed; if (it.type === "water" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 10, 8)) { @@ -461,8 +462,6 @@ function updateFirecracker(item, dt, worldRef) { panicTarinaiFromFire, igniteAntByFire, igniteGrassByFire, - igniteZunchiByFire, - tryGrassExtinguishFire, spawnFireAt: (worldRef, x, y, opts = {}) => spawnFireAt(worldRef, x, y, opts), scatterFireFrom, compactWorldFires, diff --git a/js/health.js b/js/health.js index 7ed0c23..c4fee5f 100644 --- a/js/health.js +++ b/js/health.js @@ -2,6 +2,8 @@ const HEALTH = (() => { const MAJOR_DAMAGE_WINDOW = 24; + const WEAKENED_MAJOR_DAMAGE_MIN_AGE = 3; + const WEAKENED_MAJOR_DAMAGE_MAX_AGE = 20; const MAJOR_DAMAGE_REACTION_WINDOW = 1.0; const WEAKENED_TRACE_WINDOW = 45; const WEAKENED_SUFFIX = "\u3067\u8870\u5f31"; @@ -100,6 +102,12 @@ const HEALTH = (() => { return Boolean(t.lastMajorDamageCause && now - (t.lastMajorDamageAt || -999) <= MAJOR_DAMAGE_WINDOW); } + function hasLingeringMajorDamage(t) { + const now = t.world?.time || 0; + const age = now - (t.lastMajorDamageAt || -999); + return Boolean(t.lastMajorDamageCause && age >= WEAKENED_MAJOR_DAMAGE_MIN_AGE && age < WEAKENED_MAJOR_DAMAGE_MAX_AGE); + } + function isFightingContext(t, recent) { return Boolean(t.fightTimer > 0.04 || t.defeatedTimer > 0.04 || t.defeatedById || t.fightWinnerId || recent === CAUSES.fight); } @@ -121,7 +129,7 @@ const HEALTH = (() => { if (direct === CAUSES.fire || recent === CAUSES.fire || t.lastMajorDamageCause === CAUSES.fire) return CAUSES.fire; if (recentMajor && t.lastMajorDamageCause === CAUSES.collision && !(t.energy <= 20 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) return CAUSES.collision; - if (recentMajor && (!direct || direct === t.lastMajorDamageCause || t.energy <= 18 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) { + if (hasLingeringMajorDamage(t) && (!direct || direct === t.lastMajorDamageCause || t.energy <= 18 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) { return weakenedDeathReasonFor(t.lastMajorDamageCause); } const weakenedTrace = recentDamageCause(t, WEAKENED_TRACE_WINDOW); @@ -144,7 +152,8 @@ const HEALTH = (() => { if (text.includes(CAUSES.lifespan) || text.includes("\u5bff\u547d")) return CAUSES.lifespan; if (direct) return direct; if (t.stress >= 118 || /\u30b9\u30c8\u30ec\u30b9|stress|\u6c17\u5206/.test(text)) return CAUSES.stress; - if (opts.lowHealth || t.energy <= 0.5) return CAUSES.weakness; + if ((opts.lowHealth || t.energy <= 0.5) && hasLingeringMajorDamage(t)) return weakenedDeathReasonFor(t.lastMajorDamageCause); + if (opts.lowHealth || t.energy <= 0.5) return CAUSES.accident; return ""; } @@ -155,7 +164,7 @@ const HEALTH = (() => { if (cause.includes(WEAKENED_SUFFIX)) return cause; if (opts.fromDamage) return cause; const canWeaken = cause && !cause.endsWith("\u75c5") && cause !== CAUSES.hunger && cause !== CAUSES.stress && cause !== CAUSES.lifespan && cause !== CAUSES.outOfBounds && cause !== CAUSES.collision && cause !== CAUSES.genkotsu && cause !== CAUSES.fire && cause !== CAUSES.weakness && cause !== CAUSES.freeze && cause !== CAUSES.heatstroke; - if (canWeaken && (opts.weakened || (hasRecentMajorDamage(t) && cause === t.lastMajorDamageCause))) return weakenedDeathReasonFor(cause); + if (canWeaken && (opts.weakened || (hasLingeringMajorDamage(t) && cause === t.lastMajorDamageCause))) return weakenedDeathReasonFor(cause); return cause; } diff --git a/js/item_dynamic_tool_system.js b/js/item_dynamic_tool_system.js index ab4082b..2c18c1a 100644 --- a/js/item_dynamic_tool_system.js +++ b/js/item_dynamic_tool_system.js @@ -91,13 +91,11 @@ fireTempMultiplier: (...args) => fireApi().fireTempMultiplier(...args), contactWaterDrop: (...args) => fireApi().contactWaterDrop(...args), consumeWaterForFire: (...args) => fireApi().consumeWaterForFire(...args), - igniteTarinaiByFire: (...args) => fireApi().igniteTarinaiByFire(...args), panicTarinaiFromFire: (...args) => fireApi().panicTarinaiFromFire(...args), igniteAntByFire: (...args) => fireApi().igniteAntByFire(...args), igniteGrassByFire: (...args) => fireApi().igniteGrassByFire(...args), spawnFireAt: (...args) => fireApi().spawnFireAt(...args), scatterFireFrom: (...args) => fireApi().scatterFireFrom(...args), compactWorldFires: (...args) => fireApi().compactWorldFires(...args), - updateFire: (...args) => fireApi().updateFire(...args), }); })(typeof window !== "undefined" ? window : globalThis); diff --git a/js/item_update_scheduler.js b/js/item_update_scheduler.js index 18601d8..ed784d2 100644 --- a/js/item_update_scheduler.js +++ b/js/item_update_scheduler.js @@ -55,6 +55,13 @@ ].join(":"); } + function scheduleDelay(item, interval, initial = false) { + const key = item?.id || item?._schedulerToken || item?.type || "item"; + const salt = initial ? "item-scheduler-initial" : `item-scheduler:${Math.floor((item?._schedulerLastAt || 0) * 3)}`; + const unit = typeof stableUnit === "function" ? stableUnit(key, salt) : Math.random(); + const spread = initial ? (0.42 + unit * 1.18) : (0.88 + unit * 0.24); + return interval * spread; + } function scheduleItem(state, item, now, interval = itemUpdateInterval(item)) { if (!item || item.dead || !Number.isFinite(interval)) return false; @@ -64,8 +71,9 @@ } const token = (item._schedulerToken || 0) + 1; item._schedulerToken = token; - if (!Number.isFinite(item._schedulerLastAt)) item._schedulerLastAt = now; - heapPush(state.heap, { time: now + interval, item, token }); + const initial = !Number.isFinite(item._schedulerLastAt); + if (initial) item._schedulerLastAt = now; + heapPush(state.heap, { time: now + scheduleDelay(item, interval, initial), item, token }); return true; } @@ -145,13 +153,9 @@ } function physicsBudgetFor(worldRef) { - const tier = global.TarinaiPerf.simulationQualityTier?.() || "high"; const itemCount = (worldRef?.items || []).length || 0; - const activeHint = worldRef?._itemUpdateRealtime?.length || 0; const mobile = Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760); - if (mobile && tier !== "high") return { maxPairs: 112, maxLinks: 72, postMaxPairs: 28, maxSubsteps: 2 }; if (mobile) return { maxPairs: 150, maxLinks: 96, postMaxPairs: 40, maxSubsteps: 2 }; - if (tier === "low") return { maxPairs: 140, maxLinks: 90, postMaxPairs: 36, maxSubsteps: 2 }; return itemCount > 80 ? { maxPairs: 240, maxLinks: 170, postMaxPairs: 78, maxSubsteps: 4 } : { maxPairs: 300, maxLinks: 210, postMaxPairs: 100, maxSubsteps: 5 }; diff --git a/js/main.js b/js/main.js index 8591c03..49a53c4 100644 --- a/js/main.js +++ b/js/main.js @@ -98,7 +98,7 @@ function loop() { const endRender = window.TarinaiPerf.begin("render.total"); render(); if (endRender) endRender(); - window.TarinaiPerf.endFrame(rawDt, window.__tarinaiFps || 0); + window.TarinaiPerf.endFrame(); if (window.TarinaiPerf.consumeResizeRequest() && typeof resizeCanvas === "function") resizeCanvas(); statsTimer += dt; const statsInterval = 2.5; diff --git a/js/perf_profiler.js b/js/perf_profiler.js index 34dfc9d..1ac525b 100644 --- a/js/perf_profiler.js +++ b/js/perf_profiler.js @@ -16,7 +16,7 @@ const metrics = new Map(); let frameOpen = null; let frameCount = 0; - let lastQualityCheckAt = 0; + let lastSmoothAt = 0; const storageKey = "tarinai_visual_settings_v1"; const allowed = { effects: new Set(["off", "low", "mid", "high"]), @@ -124,7 +124,7 @@ return 1; } - function endFrame(rawDt = 0, fps = global.__tarinaiFps || 0) { + function endFrame() { frameCount += 1; if (frameOpen != null && debugEnabled) { const elapsed = Math.max(0, nowMs() - frameOpen); @@ -139,8 +139,8 @@ frameOpen = null; if (debugEnabled) stack.length = 0; const t = nowMs(); - if (t - lastQualityCheckAt >= 750) { - lastQualityCheckAt = t; + if (t - lastSmoothAt >= 750) { + lastSmoothAt = t; smoothBuckets(); } } @@ -164,27 +164,8 @@ return true; } - function setVisualSettings(patch) { - let changed = false; - const next = { ...visualSettings }; - for (const [key, value] of Object.entries(patch || {})) { - if (!Object.prototype.hasOwnProperty.call(defaults, key)) continue; - const nextValue = normalizeVisualSetting(key, value); - if (!allowed[key]?.has(nextValue) || next[key] === nextValue) continue; - next[key] = nextValue; - changed = true; - if (key === "details") resizeRequested = true; - } - if (!changed) return false; - visualSettings = next; - saveVisualSettings(); - return true; - } - function renderQualityTier() { return visualSettings.details; } function shadowQualityTier() { return visualSettings.shadows; } - function effectsQualityTier() { return visualSettings.effects; } - function simulationQualityTier() { return "high"; } function dprScaleValue() { return dprScaleForDetails(visualSettings.details); } function consumeResizeRequest() { const v = resizeRequested; @@ -232,12 +213,9 @@ endFrame, renderQualityTier, shadowQualityTier, - effectsQualityTier, - simulationQualityTier, visualLevel, visualSettings: visualSettingsValue, setVisualSetting, - setVisualSettings, dprScale: dprScaleValue, consumeResizeRequest, setMetric, diff --git a/js/render.js b/js/render.js index 0871133..1d023b5 100644 --- a/js/render.js +++ b/js/render.js @@ -919,13 +919,14 @@ function syncCarriedPlushieToOwner(it, worldRef) { function collectVisibleRenderStack(worldRef, visibleRect) { const end = window.TarinaiPerf.begin("render.stackBuild"); try { - if (!worldRef._visibleRenderStack) worldRef._visibleRenderStack = { backItems: [], layered: [], carriedPlushies: [], lodgedPins: [], ballLodgedPins: [] }; + if (!worldRef._visibleRenderStack) worldRef._visibleRenderStack = { backItems: [], layered: [], carriedPlushies: [], lodgedPins: [], ballLodgedPins: [], champions: [] }; const stack = worldRef._visibleRenderStack; stack.backItems.length = 0; stack.layered.length = 0; stack.carriedPlushies.length = 0; stack.lodgedPins.length = 0; if (stack.ballLodgedPins) stack.ballLodgedPins.length = 0; + if (stack.champions) stack.champions.length = 0; const seen = new Set(); const addItem = (it) => { if (it && it.isStructure && it.type === "plushie" && it.carriedById) syncCarriedPlushieToOwner(it, worldRef); @@ -950,6 +951,7 @@ function collectVisibleRenderStack(worldRef, visibleRect) { if (!entity || entity.dead || seen.has(entity) || !isEntityVisibleInRect(entity, visibleRect)) return; seen.add(entity); stack.layered.push({ entity, y: renderLayerSortY(entity), rank: renderLayerKindRank(entity) }); + if (entity.isTarinaiChampion && typeof entity.drawChampionCrown === "function") (stack.champions || (stack.champions = [])).push(entity); }; worldRef.ensureSpatial?.("render-visible"); if (worldRef.spatial?.nearbyRectInto) { @@ -1505,7 +1507,7 @@ function render() { const endBackItems = renderPerfBegin("render.draw.backItems"); for (const it of renderStack.backItems) { - if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); + it.draw(ctx, world.time, lighting); } renderPerfEnd(endBackItems); @@ -1523,14 +1525,14 @@ function render() { if (isBall) { for (const it of renderStack.ballLodgedPins || []) { if (!it || drawnAttachedPinIds.has(it.id) || it.pinBallId !== entity.id) continue; - if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); + it.draw(ctx, world.time, lighting); drawnAttachedPinIds.add(it.id); } } if (isTarinaiEntity) { for (const it of renderStack.lodgedPins || []) { if (!it || drawnAttachedPinIds.has(it.id) || it.pinTargetId !== entity.id) continue; - if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); + it.draw(ctx, world.time, lighting); drawnAttachedPinIds.add(it.id); } } @@ -1538,31 +1540,27 @@ function render() { const endLayered = renderPerfBegin("render.draw.layered"); for (const entry of renderStack.layered) { - if (isEntityVisibleInRect(entry.entity, visibleRect)) { - entry.entity.draw(ctx, world.time, lighting); - drawAttachedPinsForEntity(entry.entity); - } + entry.entity.draw(ctx, world.time, lighting); + drawAttachedPinsForEntity(entry.entity); } renderPerfEnd(endLayered); const endAttachments = renderPerfBegin("render.draw.attachments"); for (const it of renderStack.carriedPlushies) { syncCarriedPlushieToOwner(it, world); - if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); + it.draw(ctx, world.time, lighting); } - for (const t of world.tarinai || []) { - if (t && !t.dead && t.isTarinaiChampion && typeof t.drawChampionCrown === "function" && isEntityVisibleInRect(t, visibleRect)) { - t.drawChampionCrown(ctx, world.time, lighting); - } + for (const t of renderStack.champions || []) { + t.drawChampionCrown(ctx, world.time, lighting); } for (const it of renderStack.ballLodgedPins || []) { if (!it || drawnAttachedPinIds.has(it.id)) continue; - if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); + it.draw(ctx, world.time, lighting); drawnAttachedPinIds.add(it.id); } for (const it of renderStack.lodgedPins) { if (!it || drawnAttachedPinIds.has(it.id)) continue; - if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); + it.draw(ctx, world.time, lighting); drawnAttachedPinIds.add(it.id); } renderPerfEnd(endAttachments); diff --git a/js/simulation_creature_system.js b/js/simulation_creature_system.js index f818601..a4d2a5a 100644 --- a/js/simulation_creature_system.js +++ b/js/simulation_creature_system.js @@ -16,6 +16,7 @@ const h = helpers; const end = global.TarinaiPerf.begin("update.tarinai.individual") || null; const visibleRect = h.updateVisibleWorldRect(worldRef, 160); + const nearVisibleRect = h.updateVisibleWorldRect(worldRef, 520); const now = worldRef.time || 0; const stats = { total: 0, @@ -48,7 +49,7 @@ stats.total += 1; const behaviorId = updatePolicy.behaviorId(t); inc(stats.states, behaviorId); - const cadence = updatePolicy.updateCadence(worldRef, t, visibleRect); + const cadence = updatePolicy.updateCadence(worldRef, t, visibleRect, nearVisibleRect); const interval = Number.isFinite(cadence.interval) ? cadence.interval : Infinity; inc(stats.lanes, cadence.lane || "unknown"); if (worldRef.selected === t) stats.selected += 1; diff --git a/js/simulation_effects_system.js b/js/simulation_effects_system.js index 494c062..4bd0959 100644 --- a/js/simulation_effects_system.js +++ b/js/simulation_effects_system.js @@ -3,8 +3,45 @@ // Layer: simulation/system-effects // Owns frame updates for transient visual/effect entities. (function (global) { + function updateEffect(effect, dt, frameId = 0) { + if (!effect || effect.dead) return false; + const runDt = Math.min(0.24, Math.max(0, Number(effect._effectUpdateAccum || 0) + dt)); + effect._effectUpdateAccum = 0; + effect._effectUpdatedFrame = frameId; + effect.update(runDt); + return true; + } + function updateEffects(worldRef, dt) { - for (const ef of worldRef.effects) ef.update(dt); + const effects = worldRef.effects || []; + const total = effects.length; + const frameId = (worldRef._effectUpdateFrameId || 0) + 1; + worldRef._effectUpdateFrameId = frameId; + if (total <= 56) { + for (const ef of effects) updateEffect(ef, dt, frameId); + worldRef._effectUpdateStats = { total, updated: total, skipped: 0, batched: false }; + return; + } + const budget = Math.max(40, Math.ceil(total * 0.42)); + let cursor = Math.max(0, Math.floor(worldRef._effectUpdateCursor || 0)) % total; + let updated = 0; + let scanned = 0; + while (updated < budget && scanned < total) { + const ef = effects[cursor]; + cursor = (cursor + 1) % total; + scanned += 1; + if (!ef || ef.dead) continue; + updateEffect(ef, dt, frameId); + updated += 1; + } + for (let i = 0; i < total; i += 1) { + const ef = effects[i]; + if (!ef || ef.dead || ef._effectUpdatedFrame === frameId) continue; + if (Number.isFinite(ef._effectUpdateAccum)) ef._effectUpdateAccum = Math.min(0.24, ef._effectUpdateAccum + dt); + else ef._effectUpdateAccum = dt; + } + worldRef._effectUpdateCursor = cursor; + worldRef._effectUpdateStats = { total, updated, skipped: Math.max(0, total - updated), batched: true }; } global.TarinaiEffectsSimulationSystem = Object.freeze({ diff --git a/js/simulation_environment_system.js b/js/simulation_environment_system.js index 4c4dc74..a5db8b4 100644 --- a/js/simulation_environment_system.js +++ b/js/simulation_environment_system.js @@ -38,7 +38,11 @@ worldRef.updateWeather(dt); worldRef.updateTemperature?.(dt); worldRef.updateTerrainDirtyState?.(dt); - worldRef.evaluateColonyMood?.(); + const now = worldRef.time || 0; + if (now >= (worldRef.nextColonyMoodEvaluateAt || 0)) { + worldRef.nextColonyMoodEvaluateAt = now + 0.65; + worldRef.evaluateColonyMood?.(); + } } function prepareSpatialFrame(worldRef) { diff --git a/js/simulation_maintenance_system.js b/js/simulation_maintenance_system.js index c473d2a..175ae1b 100644 --- a/js/simulation_maintenance_system.js +++ b/js/simulation_maintenance_system.js @@ -17,10 +17,15 @@ } worldRef.countsTimer += dt; - const countsInterval = 2.0; - if (worldRef.countsTimer >= countsInterval || itemsCompacted || worldRef.items.length !== itemCountBefore) { - worldRef.updateItemCounts(); - worldRef.updateEffectCounts(); + const countsInterval = 2.75; + const itemCountChanged = worldRef.items.length !== itemCountBefore || worldRef.items.length !== (worldRef._lastMaintenanceItemCount ?? worldRef.items.length); + const effectCountChanged = worldRef.effects.length !== (worldRef._lastMaintenanceEffectCount ?? worldRef.effects.length); + const countsDue = worldRef.countsTimer >= countsInterval; + if (countsDue || itemsCompacted || itemCountChanged || effectCountChanged || worldRef.itemBucketsDirty) { + if (countsDue || itemsCompacted || itemCountChanged || worldRef.itemBucketsDirty) worldRef.updateItemCounts(); + if (countsDue || itemsCompacted || effectCountChanged) worldRef.updateEffectCounts(); + worldRef._lastMaintenanceItemCount = worldRef.items.length; + worldRef._lastMaintenanceEffectCount = worldRef.effects.length; worldRef.countsTimer = 0; } if ((worldRef.time || 0) >= (worldRef.nextGrassLimitCheckAt || 0)) { @@ -71,7 +76,8 @@ } worldRef.drawSortTimer += dt; - const sortInterval = 0.25; + const visibleMoving = Boolean(worldRef.spatialTarinaiDirty || worldRef.spatialDynamicItemsDirty || worldRef.spatialAntDirty); + const sortInterval = visibleMoving ? 0.25 : 0.55; if (worldRef.drawSortTimer >= sortInterval) { worldRef.drawListDirty = true; worldRef.drawSortTimer = 0; diff --git a/js/tarinai_item_targeting.js b/js/tarinai_item_targeting.js index 6a9abd0..8e27e30 100644 --- a/js/tarinai_item_targeting.js +++ b/js/tarinai_item_targeting.js @@ -11,7 +11,7 @@ function isDuplicatorFoodSource(item) { return !!item && item.type === "duplicator"; } -const NORMAL_POOP_MEAL_THRESHOLD = 3; +const NORMAL_POOP_MEAL_THRESHOLD = 4; function normalPoopUnitsFor(foodType = "", amount = 0, options = {}) { const type = String(foodType || ""); diff --git a/js/tarinai_update_policy.js b/js/tarinai_update_policy.js index e509650..6b3b861 100644 --- a/js/tarinai_update_policy.js +++ b/js/tarinai_update_policy.js @@ -63,18 +63,17 @@ return false; } - function cadence(worldRef, tarinai, visibleRect) { + function cadence(worldRef, tarinai, visibleRect, nearVisibleRect = null) { if (!tarinai || tarinai.dead) return { interval: Infinity, lane: "dead", smoothMotion: false, visible: false, urgent: false }; const isSelected = worldRef.selected === tarinai; if (isSelected) return { interval: 0, lane: "selected", smoothMotion: false, visible: true, urgent: true }; const id = behaviorId(tarinai); const state = String(tarinai.state || id || ""); - const tier = global.TarinaiPerf.simulationQualityTier?.() || "high"; const count = (worldRef.tarinai || []).length; const mobile = Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760); const isVisible = visible(worldRef, tarinai, visibleRect); - const farRect = isVisible ? visibleRect : helpers.updateVisibleWorldRect(worldRef, 520); + const farRect = isVisible ? visibleRect : (nearVisibleRect || helpers.updateVisibleWorldRect(worldRef, 520)); const isNear = isVisible || Boolean(helpers.pointInRect(tarinai.x || 0, tarinai.y || 0, farRect)); const motionNeed = hasMotionNeed(tarinai); @@ -95,7 +94,7 @@ // runs on visible skipped frames. const panicUrgent = (tarinai.fearTimer || 0) > 0.20 || /panic/i.test(id); if (panicUrgent) { - const interval = isVisible ? (mobile ? 0.13 : (tier === "low" ? 0.12 : 0.08)) : (isNear ? (mobile ? 0.24 : 0.18) : (mobile ? 0.46 : 0.36)); + const interval = isVisible ? (mobile ? 0.13 : 0.08) : (isNear ? (mobile ? 0.24 : 0.18) : (mobile ? 0.46 : 0.36)); return { interval, lane: "panic", smoothMotion: Boolean(isVisible && !worldRef.isTarinaiHiddenInNestBox?.(tarinai)), visible: isVisible, urgent: true }; } @@ -104,10 +103,10 @@ if (/birth_ritual/i.test(id) || state === "birth_ritual" || (tarinai.birthRitualTimer || 0) > 0.04) { lane = "birth"; - interval = isVisible ? (tier === "low" ? 0.16 : 0.12) : 0.24; + interval = isVisible ? 0.12 : 0.24; } else if (/approach_parent_or_child|follow_parent|approach_mate|seek_bed|play_ball|seek_material|build/i.test(id) || /follow_parent|seek_bed|play_ball/.test(state)) { lane = "moving"; - interval = isVisible ? (tier === "low" ? 0.14 : 0.10) : (isNear ? 0.22 : 0.55); + interval = isVisible ? 0.10 : (isNear ? 0.22 : 0.55); } else if (/eat_food|eat/i.test(id) || state === "eat" || (tarinai.eatTimer || 0) > 0.04) { lane = "eat"; interval = isVisible ? 0.22 : 0.48; @@ -116,16 +115,20 @@ interval = isVisible ? 0.42 : (isNear ? 0.70 : 1.10); } else if (/idle|none/.test(id) || state === "idle") { lane = "idle"; - if (isVisible) interval = tier === "low" ? 0.24 : (count > 28 ? 0.16 : 0.12); + if (isVisible) interval = count > 28 ? 0.16 : 0.12; else interval = isNear ? 0.55 : 1.10; } else if (isVisible) { lane = "visible"; - interval = tier === "low" ? (count > 24 ? 0.18 : 0.14) : (count > 36 ? 0.11 : (count > 24 ? 0.08 : 0.05)); + interval = count > 36 ? 0.11 : (count > 24 ? 0.08 : 0.05); } else { lane = isNear ? "near" : "far"; - interval = isNear ? (tier === "low" ? 0.40 : 0.30) : (tier === "low" ? 1.20 : 0.90); + interval = isNear ? 0.30 : 0.90; } + if (Number.isFinite(interval) && !/birth|eat|sleep/.test(lane)) { + if (count > 54) interval *= 1.26; + else if (count > 38) interval *= 1.14; + } if (mobile && Number.isFinite(interval)) interval *= count > 24 ? 1.35 : 1.18; const passivePhysicsNeed = hasPassivePhysicsNeed(tarinai, worldRef, isVisible); const sleepPhysical = lane === "sleep" && passivePhysicsNeed; diff --git a/js/version.js b/js/version.js index f9db371..87d0752 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "39.15.107"; - const APP_BUILD = "tarinai-family-line-lanes-v39-15-107"; + const APP_VERSION = "39.15.110"; + const APP_BUILD = "tarinai-fps-cadence-v39-15-110"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; diff --git a/js/world_reset_presets.js b/js/world_reset_presets.js index d28dc11..fe53965 100644 --- a/js/world_reset_presets.js +++ b/js/world_reset_presets.js @@ -336,18 +336,38 @@ const RESET_PRESETS = Object.freeze({ addPresetItemsScattered(worldRef, "ball", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-ball" }); } + function addDefaultPresetRobotCleaner(worldRef, presetId = "default") { + if (!worldRef || normalizeResetPresetId(presetId) === "empty") return; + if ((worldRef.items || []).some(item => item && !item.dead && item.type === "robot_cleaner")) return; + const center = { x0: worldRef.w * 0.16, x1: worldRef.w * 0.84, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 }; + addPresetItemsScattered(worldRef, "robot_cleaner", 1, center, { + pad: 112, + reason: `preset-${normalizeResetPresetId(presetId)}-default-robot-cleaner`, + avoidOverlap: true, + avoidAllObstacles: true, + attempts: 140, + maxRadius: 260, + }); + if ((worldRef.items || []).some(item => item && !item.dead && item.type === "robot_cleaner")) return; + const p = randPoint(worldRef, 88, center); + addPresetItem(worldRef, "robot_cleaner", p.x, p.y, { reason: `preset-${normalizeResetPresetId(presetId)}-default-robot-cleaner` }); + } + function generatePresetWorld(worldRef, field, presetId, seedPopulation) { - switch (normalizeResetPresetId(presetId)) { + const preset = normalizeResetPresetId(presetId); + switch (preset) { case "empty": return; - case "athletic": return generateAthleticPreset(worldRef, field); - case "crowded": return generateCrowdedPreset(worldRef, field); - case "family": return generateFamilyPreset(worldRef, field); - case "war": return generateWarPreset(worldRef, field); - case "happy": return generateHappyPreset(worldRef, field); + case "athletic": generateAthleticPreset(worldRef, field); break; + case "crowded": generateCrowdedPreset(worldRef, field); break; + case "family": generateFamilyPreset(worldRef, field); break; + case "war": generateWarPreset(worldRef, field); break; + case "happy": generateHappyPreset(worldRef, field); break; case "default": default: - return generateDefaultPreset(worldRef, field, seedPopulation); + generateDefaultPreset(worldRef, field, seedPopulation); + break; } + addDefaultPresetRobotCleaner(worldRef, preset); } function groundForResetPreset(presetId = "default") { diff --git a/js/world_spatial_budget.js b/js/world_spatial_budget.js index 4622ab9..052d889 100644 --- a/js/world_spatial_budget.js +++ b/js/world_spatial_budget.js @@ -116,6 +116,7 @@ }, scheduler: this._itemUpdateSchedulerStats || null, creatures: this._creatureUpdateStats || null, + effects: this._effectUpdateStats || null, terrain: this.terrainDirtyStatsThisFrame || null, nearby: this.nearbyQueryStatsThisFrame || null, constraintDirty: this.constraintDirtyStatsThisFrame || null, diff --git a/service-worker.js b/service-worker.js index 7c27b35..31f23bf 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,6 +1,6 @@ "use strict"; -const APP_VERSION = "39.15.107"; +const APP_VERSION = "39.15.110"; const CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const v = `v=${APP_VERSION}`; // Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.