diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c69165 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.tmp-edge-profile/ +__pycache__/ +*.pyc + +# Local browser smoke/probe artifacts. +*probe*.html +*probe*.png +*smoke*.html +*smoke*.png diff --git a/.tmp-edge-profile/Crashpad/metadata b/.tmp-edge-profile/Crashpad/metadata deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-edge-profile/Crashpad/settings.dat b/.tmp-edge-profile/Crashpad/settings.dat deleted file mode 100644 index e76f11a..0000000 Binary files a/.tmp-edge-profile/Crashpad/settings.dat and /dev/null differ diff --git a/.tmp-edge-profile/Crashpad/throttle_store.dat b/.tmp-edge-profile/Crashpad/throttle_store.dat deleted file mode 100644 index 9639e1b..0000000 --- a/.tmp-edge-profile/Crashpad/throttle_store.dat +++ /dev/null @@ -1 +0,0 @@ -level=none expiry=0 diff --git a/.tmp-edge-profile/Variations b/.tmp-edge-profile/Variations deleted file mode 100644 index a157215..0000000 --- a/.tmp-edge-profile/Variations +++ /dev/null @@ -1 +0,0 @@ -{"user_experience_metrics.stability.exited_cleanly":false,"variations_crash_streak":0} \ No newline at end of file diff --git a/assets/objects/plushie.webp b/assets/objects/plushie.webp deleted file mode 100644 index fcfb4c2..0000000 Binary files a/assets/objects/plushie.webp and /dev/null differ diff --git a/css/base.css b/css/base.css index d773199..adc7103 100644 --- a/css/base.css +++ b/css/base.css @@ -442,7 +442,7 @@ h1 { } .signboard-editor.hidden { display: none; } .signboard-editor-panel { - width: min(430px, calc(100vw - 28px)); + width: min(220px, calc(100vw - 28px)); border-radius: 18px; padding: 14px; background: linear-gradient(180deg, rgba(255,253,247,0.98), rgba(245,237,221,0.96)); @@ -476,8 +476,11 @@ h1 { } .signboard-editor-close:hover { background: rgba(255,248,232,0.96); } #signboardEditorText { - width: 100%; + width: 7.4em; + max-width: 100%; box-sizing: border-box; + display: block; + margin: 0 auto; resize: none; min-height: 88px; max-height: 88px; @@ -498,6 +501,8 @@ h1 { } .signboard-editor-meta { display: flex; + flex-wrap: wrap; + gap: 2px 8px; justify-content: space-between; color: var(--muted); font-size: 12px; @@ -505,8 +510,8 @@ h1 { } .signboard-editor-actions { display: flex; - justify-content: flex-end; + flex-wrap: wrap; + justify-content: center; gap: 8px; margin-top: 12px; } - diff --git a/js/data.js b/js/data.js index 127727e..b91fd99 100644 --- a/js/data.js +++ b/js/data.js @@ -44,7 +44,6 @@ const DECOR_ASSETS = [ { id: "pushpin_stuck", path: "assets/objects/pushpin_stuck.webp" }, { id: "oshibyo", path: "assets/objects/oshibyo.webp" }, { id: "oshibyo_stuck", path: "assets/objects/oshibyo_stuck.webp" }, - { id: "plushie", path: "assets/objects/plushie.webp" }, ]; const TOOL_DEFINITIONS = Object.freeze({ diff --git a/js/debug_tools.js b/js/debug_tools.js index c8b6869..898a7b8 100644 --- a/js/debug_tools.js +++ b/js/debug_tools.js @@ -24,15 +24,18 @@ const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {}; const behaviorCounts = diag.behavior?.counts || {}; const topBehaviors = Object.entries(behaviorCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([id, n]) => `${id}:${n}`).join(", "); + const quality = diag.performance?.quality || perf.quality || {}; panel.textContent = [ `tarinai v${window.TARINAI_VERSION || "?"}`, `fps ${perf.fps ?? window.__tarinaiFps ?? "?"} perf ${perf.level ?? "?"}`, + `live ${diag.performance?.liveTarinai ?? quality.tarinaiCount ?? (w?.tarinai || []).length} qlevel ${quality.level ?? diag.performance?.level ?? "?"} pressure ${Number(diag.performance?.pressure ?? quality.pressure ?? 0).toFixed(2)} dpr ${diag.performance?.dpr || uiCache?.canvasDpr || "?"} maxDpr ${Number(quality.maxDpr ?? 0).toFixed ? Number(quality.maxDpr ?? 0).toFixed(2) : "?"}`, `items ${(w?.items || []).length} tarinai ${(w?.tarinai || []).length} ants ${(w?.ants || []).length}`, `effects ${(w?.effects || []).length}`, `terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0}`, `spatial rebuild ${diag.spatial?.rebuildsThisFrame ?? 0}/frame total:${diag.spatial?.rebuildsTotal ?? 0} dirty:${diag.spatial?.dirtyMarksThisFrame ?? 0} reason:${diag.spatial?.dirtyReason || diag.spatial?.lastRebuildReason || "-"}`, `drawList ${(w?.drawList || []).length} dirty:${Boolean(diag.render?.drawListDirty)} logs ${(w?.logs || []).length}`, `item buckets:${diag.items?.bucketTypes ?? 0} dirty:${Boolean(diag.items?.bucketsDirty)} id-map:${diag.items?.idMapSize ?? 0} rebuilds:${diag.items?.bucketRebuildsTotal ?? 0}`, + `work ai ${diag.work?.stats?.aiRuns ?? 0}/${diag.work?.stats?.aiSkips ?? 0} env ${diag.work?.stats?.envRuns ?? 0}/${diag.work?.stats?.envSkips ?? 0} coll ${diag.work?.stats?.collisionRuns ?? 0}/${diag.work?.stats?.collisionSkips ?? 0}`, `behavior forced:${diag.behavior?.forced ?? 0} locked:${diag.behavior?.locked ?? 0} top ${topBehaviors || "-"}`, `dpr ${uiCache?.canvasDpr || "?"} cache ${window.TARINAI_APP?.cacheName || "?"}`, `sw ${navigator.serviceWorker?.controller ? "controlled" : "uncontrolled"} soundPack ${window.TarinaiAudio?.soundPackVersion || "?"}`, diff --git a/js/main.js b/js/main.js index 8003425..8df5582 100644 --- a/js/main.js +++ b/js/main.js @@ -5,6 +5,7 @@ let fpsTimer = 0; let fpsFrames = 0; let perfResizeTimer = 0; let lastPerfLevel = 0; +let lastPerfMaxDpr = 0; window.__tarinaiFps = 0; const LOADING_SPRITES = [ @@ -91,11 +92,14 @@ function loop() { world.update(dt); render(); statsTimer += dt; - const perf = world.performanceLevel ? world.performanceLevel() : 0; + const quality = world.performanceQuality?.() || {}; + const perf = Number(quality.level ?? (world.performanceLevel ? world.performanceLevel() : 0)) || 0; perfResizeTimer += rawDt; if (perfResizeTimer > 1.5) { - if (perf !== lastPerfLevel) { + const maxDpr = Number(quality.maxDpr || 0) || 0; + if (perf !== lastPerfLevel || Math.abs(maxDpr - lastPerfMaxDpr) > 0.08) { lastPerfLevel = perf; + lastPerfMaxDpr = maxDpr; resizeCanvas(); } perfResizeTimer = 0; @@ -139,4 +143,3 @@ if ("serviceWorker" in navigator) { }); } - diff --git a/js/performance_manager.js b/js/performance_manager.js index 8c6e27b..2710873 100644 --- a/js/performance_manager.js +++ b/js/performance_manager.js @@ -7,6 +7,7 @@ class TarinaiPerformanceManager { this.lastLevel = 0; this.samples = []; this.sampleLimit = 20; + this.pressure = 0; } updateFps(fps, worldRef = null) { @@ -16,7 +17,8 @@ class TarinaiPerformanceManager { 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); - const next = this.computeLevel(avg); + this.pressure = this.computePressure(avg, worldRef); + const next = this.computeLevel(avg, worldRef); if (next !== this.level) { this.lastLevel = this.level; this.level = next; @@ -25,33 +27,68 @@ class TarinaiPerformanceManager { return this.level; } - computeLevel(fps = this.fps) { - if (!fps || !Number.isFinite(fps)) return 0; - if (fps < 22) return 3; - if (fps < 34) return 2; - if (fps < 48) return 1; + 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; } - quality(worldRef = null) { - const level = this.level; + computePressure(fps = this.fps, worldRef = null) { + const q = this.countWorkload(worldRef); + const n = Number(fps); + const fpsPressure = Number.isFinite(n) && n > 0 ? clamp((58 - n) / 38, 0, 1) : 0; + const workPressure = clamp(Math.log1p(q.workUnits / 42) / Math.log1p(260 / 42), 0, 1); + const sampleBoost = this.samples.length < 4 ? 0.84 : 1.0; + return clamp(Math.max(fpsPressure, workPressure * 0.78) * sampleBoost, 0, 1); + } + + countWorkload(worldRef = null) { + let tarinaiCount = 0; + for (const t of worldRef?.tarinai || []) if (t && !t.dead) tarinaiCount += 1; let antCount = 0; for (const ant of worldRef?.ants || []) if (ant && !ant.dead) antCount += 1; const itemCount = (worldRef?.items || []).length || 0; const effectCount = (worldRef?.effects || []).length || 0; - const antStride = antCount >= 95 || level >= 3 ? 3 : (antCount >= 35 || level >= 2 ? 2 : 1); + 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, - antCount, - effectCount, - effectLimit: level >= 3 ? 34 : level >= 2 ? 52 : level >= 1 ? 68 : 88, - statsInterval: level >= 3 ? 1.20 : level === 2 ? 0.92 : level === 1 ? 0.68 : 0.48, - terrainRedrawInterval: level >= 3 ? 1.30 : level === 2 ? 1.05 : level === 1 ? 0.82 : 0.62, - backgroundLightStep: level >= 3 ? 64 : level === 2 ? 96 : 180, - uiUpdateInterval: level >= 3 ? 1.2 : level === 2 ? 0.9 : level === 1 ? 0.65 : 0.45, + 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, }; } diff --git a/js/render.js b/js/render.js index 4d775dc..234ced6 100644 --- a/js/render.js +++ b/js/render.js @@ -704,8 +704,9 @@ function drawAtmosphericLighting(ctx, w, h, lighting) { function drawRain(ctx, w, h, t) { if (world.weather !== "light_rain") return; ctx.save(); - const perf = world.performanceLevel ? world.performanceLevel() : 0; - const count = perf >= 2 ? 64 : 138; + const quality = world.performanceQuality?.() || {}; + const density = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1); + const count = Math.max(20, Math.round(138 * density)); const angle = -0.32; // Rain visual effect: diagonal streaks, separate from fallen water item sprites. const slant = Math.tan(angle); for (let i = 0; i < count; i++) { @@ -729,7 +730,7 @@ function drawRain(ctx, w, h, t) { ctx.moveTo(x - dx * 0.5, y - dy * 0.5); ctx.lineTo(x + dx * 0.5, y + dy * 0.5); ctx.stroke(); - if (perf < 3 && i % 4 === 0) { + if (density > 0.25 && i % 4 === 0) { ctx.globalAlpha = 0.13 * alpha; ctx.strokeStyle = "rgba(255,255,255,0.70)"; ctx.lineWidth = 0.7; @@ -795,8 +796,9 @@ function render() { } ctx.restore(); - const perf = world.performanceLevel ? world.performanceLevel() : 0; - if (perf < 2) drawLightRays(ctx, screenW, screenH, lighting); + const quality = world.performanceQuality?.() || {}; + 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 terrainLayer = ensureTerrainCache(world, lighting); @@ -812,21 +814,23 @@ function render() { if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); } - ctx.save(); - ctx.lineWidth = 1.15; - ctx.setLineDash([4, 4]); - ctx.strokeStyle = light > 0.42 ? "rgba(115, 151, 86, 0.32)" : "rgba(202, 220, 255, 0.22)"; - for (const t of world.tarinai) { - if (!isEntityVisibleInRect(t, visibleRect, 80)) continue; - const p = t.parentToFollow?.(); - if (!p || !t.juvenile || t.dead || p.dead || dist(t, p) > 96 || !isEntityVisibleInRect(p, visibleRect, 80)) continue; - ctx.beginPath(); - ctx.moveTo(t.x, t.y); - ctx.quadraticCurveTo((t.x + p.x) * 0.5, Math.min(t.y, p.y) - 10, p.x, p.y); - ctx.stroke(); + if (density > 0.45) { + ctx.save(); + ctx.lineWidth = 1.15; + ctx.setLineDash([4, 4]); + ctx.strokeStyle = light > 0.42 ? "rgba(115, 151, 86, 0.32)" : "rgba(202, 220, 255, 0.22)"; + for (const t of world.tarinai) { + if (!isEntityVisibleInRect(t, visibleRect, 80)) continue; + const p = t.parentToFollow?.(); + if (!p || !t.juvenile || t.dead || p.dead || dist(t, p) > 96 || !isEntityVisibleInRect(p, visibleRect, 80)) continue; + ctx.beginPath(); + ctx.moveTo(t.x, t.y); + ctx.quadraticCurveTo((t.x + p.x) * 0.5, Math.min(t.y, p.y) - 10, p.x, p.y); + ctx.stroke(); + } + ctx.setLineDash([]); + ctx.restore(); } - ctx.setLineDash([]); - ctx.restore(); for (const entry of renderStack.layered) { if (isEntityVisibleInRect(entry.entity, visibleRect)) entry.entity.draw(ctx, world.time, lighting); diff --git a/js/snapshot_system.js b/js/snapshot_system.js index 13ecf0b..f704f0a 100644 --- a/js/snapshot_system.js +++ b/js/snapshot_system.js @@ -263,7 +263,7 @@ function itemExtra(item, tarinaiIndex = new Map()) { const type = item?.type || ""; - if (item?.isStructure) return ["S", q(item.hp, 10), q(item.maxHp, 10), q(item.attachment, 10), q(item.usedCount, 1), tarinaiIndex.get(item.ownerId) ?? -1, item.carriedById ? (tarinaiIndex.get(item.carriedById) ?? -1) : -1, item.onHead ? 1 : 0]; + 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 === "bed") return [q(item.comfort, 1000), q(item.wear, 1000)]; if (type === "signboard") return [item.text || ""]; @@ -296,6 +296,7 @@ item.ownerId = tarinaiList[extra[5]]?.id || ""; item.carriedById = tarinaiList[extra[6]]?.id || ""; item.onHead = !!extra[7]; + item.plushieSpriteId = extra[8] || item.plushieSpriteId || ""; const def = global.StructureRegistry?.get?.(type); if (def) { item.label = def.label; diff --git a/js/structures.js b/js/structures.js index 9cb5b3f..6157650 100644 --- a/js/structures.js +++ b/js/structures.js @@ -29,6 +29,10 @@ this.needEffects = clonePlain(definition.needEffects); this.carriedById = definition.type === "plushie" ? this.ownerId : ""; this.onHead = definition.type === "plushie"; + if (definition.type === "plushie") { + const sprites = typeof spawnableSprites === "function" ? spawnableSprites().map(s => s.id).filter(Boolean) : []; + this.plushieSpriteId = sprites.length ? sprites[Math.floor(Math.random() * sprites.length)] : (owner?.type || "smile"); + } this.dead = false; this.world = world || null; } @@ -106,10 +110,11 @@ ctx.ellipse(0, this.r * 0.22, this.r * 1.28, this.r * 0.28, -0.06, 0, Math.PI * 2); ctx.fill(); } else if (this.type === "plushie") { - const img = typeof getRenderableImage === "function" ? getRenderableImage("plushie", "plushie") : global.images?.get?.("plushie"); + const spriteId = this.plushieSpriteId || "smile"; + const img = typeof getRenderableImage === "function" ? getRenderableImage(spriteId, "smile") : (global.images?.get?.(spriteId) || global.images?.get?.("smile")); ctx.rotate(Math.sin(time * 2 + this.seed) * 0.05); if (img) { - const metrics = typeof getImageMetrics === "function" ? getImageMetrics("plushie") : null; + const metrics = typeof getImageMetrics === "function" ? getImageMetrics(spriteId) || getImageMetrics("smile") : null; const w = this.r * 3.0; const h = w * (metrics?.ratio || 0.74); ctx.drawImage(img, -w * 0.58, -h * 0.60, w, h); diff --git a/js/tarinai_identity_social.js b/js/tarinai_identity_social.js index 3f4068d..5c8cd84 100644 --- a/js/tarinai_identity_social.js +++ b/js/tarinai_identity_social.js @@ -166,14 +166,14 @@ rel.fightsWon = rel.fightsWon || 0; rel.fightsLost = rel.fightsLost || 0; if (event) { rel.lastEvent = event; rel.lastTime = this.world.time; } + this.relationCache = null; }, strongestRelation(kind = "friend", liveOnly = true) { let bestId = null; let bestScore = kind === "fear" ? 5 : FRIEND_AFFINITY_THRESHOLD; - const liveIds = liveOnly ? new Set((this.world?.tarinai || []).filter(o => o && !o.dead && o !== this).map(o => o.id).filter(Boolean)) : null; for (const [id, rel] of Object.entries(this.relationships || {})) { - if (liveIds && !liveIds.has(id)) continue; + if (liveOnly && (!this.world?.liveTarinaiById?.(id) || id === this.id)) continue; const score = kind === "fear" ? (rel.fear || 0) : (rel.affinity || 0); if (score > bestScore) { bestId = id; bestScore = score; } } @@ -187,13 +187,16 @@ }, currentFriendCount(minScore = FRIEND_AFFINITY_THRESHOLD, liveOnly = true) { + const now = this.world?.time || 0; + const cacheKey = `${minScore}:${liveOnly ? 1 : 0}`; + if (this.relationCache?.friendCountKey === cacheKey && now < (this.relationCache.friendCountUntil || 0)) return this.relationCache.friendCount; let count = 0; - const liveIds = liveOnly ? new Set((this.world?.tarinai || []).filter(o => o && !o.dead && o !== this).map(o => o.id)) : null; for (const [id, rel] of Object.entries(this.relationships || {})) { if ((rel.affinity || 0) <= minScore) continue; - if (liveIds && !liveIds.has(id)) continue; + if (liveOnly && (!this.world?.liveTarinaiById?.(id) || id === this.id)) continue; count += 1; } + this.relationCache = { ...(this.relationCache || {}), friendCountKey: cacheKey, friendCount: count, friendCountUntil: now + 1.4 }; return count; }, @@ -203,16 +206,23 @@ }, bestFriendLive(minScore = FRIEND_AFFINITY_THRESHOLD, maxDist = Infinity) { + const now = this.world?.time || 0; + const cacheKey = `${minScore}:${Number.isFinite(maxDist) ? Math.round(maxDist) : "inf"}`; + if (this.relationCache?.bestFriendKey === cacheKey && now < (this.relationCache.bestFriendUntil || 0)) { + const cached = this.world?.liveTarinaiById?.(this.relationCache.bestFriendId); + if (cached && (!Number.isFinite(maxDist) || dist(this, cached) <= maxDist)) return cached; + } let best = null, bestScore = minScore; for (const [id, rel] of Object.entries(this.relationships || {})) { const score = rel.affinity || 0; if (score <= bestScore) continue; - const t = this.world.tarinai.find(o => o.id === id && !o.dead); + const t = this.world.liveTarinaiById?.(id) || null; if (!t) continue; if (Number.isFinite(maxDist) && dist(this, t) > maxDist) continue; best = t; bestScore = score; } + this.relationCache = { ...(this.relationCache || {}), bestFriendKey: cacheKey, bestFriendId: best?.id || "", bestFriendUntil: now + 1.0 }; return best; }, @@ -223,7 +233,7 @@ const when = rel.lastTime || -Infinity; if (this.world.time - when > maxAge) continue; if (!(event.includes("fight") || event.includes("\u55a7\u5629") || (rel.fightsWon || 0) || (rel.fightsLost || 0))) continue; - const t = this.world.tarinai.find(o => o.id === id && !o.dead); + const t = this.world.liveTarinaiById?.(id) || null; if (!t) continue; if (Number.isFinite(maxDist) && dist(this, t) > maxDist) continue; if (when > bestT) { best = t; bestT = when; } diff --git a/js/tarinai_needs_items.js b/js/tarinai_needs_items.js index 4cdd6ce..4e49c70 100644 --- a/js/tarinai_needs_items.js +++ b/js/tarinai_needs_items.js @@ -122,13 +122,26 @@ function needPressure(value) { } function calculateStressFromNeeds(needs) { - return clamp( + const weightedLinear = + (Number(needs.food) || 0) * 0.09 + + (Number(needs.sleep) || 0) * 0.11 + + (Number(needs.health) || 0) * 0.22 + + (Number(needs.safety) || 0) * 0.26 + + (Number(needs.social) || 0) * 0.07 + + (Number(needs.fulfill) || 0) * 0.13; + const peakNeed = Math.max(0, ...TARINAI_NEED_KEYS.map(key => Number(needs?.[key] || 0) || 0)); + const peakPressure = Math.max(0, peakNeed - 62) * 0.10; + const quadraticPressure = needPressure(needs.food) * 0.10 + needPressure(needs.sleep) * 0.12 + needPressure(needs.health) * 0.25 + needPressure(needs.safety) * 0.30 + needPressure(needs.social) * 0.08 + - needPressure(needs.fulfill) * 0.15, + needPressure(needs.fulfill) * 0.15; + return clamp( + weightedLinear + + peakPressure + + quadraticPressure * 0.35, 0, 130 ); @@ -1392,7 +1405,8 @@ function createSleepBuildGrassBedActionSpec() { timerBacked: true, selectTarget: selectRoleTarget("grassMaterial", 520), canStart(t, world, ctx = {}) { - return !findNearestSleepPlace(world, t, 760) + return canBuildOwnedStructureByAge(t) + && !findNearestSleepPlace(world, t, 760) && !findOwnedStructure(world, t, "grass_bed") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["fight", "panic", "intimidate", "birth_ritual"].includes(t.state); @@ -1780,7 +1794,7 @@ const TARINAI_ACTION_DEFINITIONS = [ phrase: "かんたんベッドを作った", weight: 34, selectTarget: selectRoleTarget("grassMaterial", 520), - canStart(t, world, ctx = {}) { return !findOwnedStructure(world, t, "grass_bed") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); }, + canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "grass_bed") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); }, start(t, world) { return buildOwnedStructureFromGrass(t, world, "grass_bed", this.label); }, }, { @@ -1791,7 +1805,7 @@ const TARINAI_ACTION_DEFINITIONS = [ phrase: "ぬいぐるみを作った", weight: 28, selectTarget: selectRoleTarget("grassMaterial", 520), - canStart(t, world, ctx = {}) { return !findOwnedStructure(world, t, "plushie") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); }, + canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "plushie") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); }, start(t, world) { return buildOwnedStructureFromGrass(t, world, "plushie", this.label); }, }, { @@ -2065,6 +2079,32 @@ function structureBuildProbe(type, x, y) { return { type, x, y, r, dead: false, isStructure: true, roles: def.roles || {}, ownerId: "" }; } +function canBuildOwnedStructureByAge(tarinai) { + if (!tarinai || tarinai.dead) return false; + if (tarinai.generation <= 1) return true; + const growth = Number.isFinite(tarinai.growth) + ? tarinai.growth + : clamp(((tarinai.world?.time || 0) - (tarinai.birthTime || 0)) / Math.max(1, CONFIG.childGrowTime || 58), 0, 1); + return growth >= 0.55; +} + +function stopFailedBuildPlan(t, world, type = "", reason = "") { + if (!t) return false; + t.buildPlan = null; + t.actionCooldowns = t.actionCooldowns || {}; + t.actionIdCooldowns = t.actionIdCooldowns || {}; + const actionId = type === "grass_bed" ? "build_grass_bed" : (type === "plushie" ? "build_plushie" : "build"); + t.actionCooldowns.fulfill = Math.max(t.actionCooldowns.fulfill || 0, 5.0); + t.actionCooldowns.sleep = Math.max(t.actionCooldowns.sleep || 0, type === "grass_bed" ? 3.8 : 0); + t.actionIdCooldowns[actionId] = Math.max(t.actionIdCooldowns[actionId] || 0, 9.0); + if (type === "grass_bed") t.actionIdCooldowns.sleep_build_grass_bed = Math.max(t.actionIdCooldowns.sleep_build_grass_bed || 0, 9.0); + t.behaviorSwitchCooldownUntil = Math.max(t.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 1.2); + const text = reason || "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044"; + if (typeof clearActiveBehavior === "function") clearActiveBehavior(t, text); + t.goIdle?.(text); + return false; +} + function findStructureBuildSpot(t, world, type) { const baseX = Number.isFinite(t?.x) ? t.x : 0; const baseY = Number.isFinite(t?.y) ? t.y : 0; @@ -2117,8 +2157,7 @@ function continueBuildPlan(t, world, dt) { if (material && (material.dead || !roleMatches(material, "grassMaterial"))) material = null; if (!material) material = findNearbyMaterial(world, t, "grassMaterial", 520); if (!material) { - t.buildPlan = null; - return false; + return stopFailedBuildPlan(t, world, plan.type, "\u6750\u6599\u304c\u898b\u3064\u304b\u3089\u306a\u3044"); } plan.materialId = material.id; const label = globalThis.StructureRegistry.get(plan.type)?.label || "\u69cb\u9020\u7269"; @@ -2133,8 +2172,7 @@ function continueBuildPlan(t, world, dt) { const spot = findStructureBuildSpot(t, world, plan.type); const structure = globalThis.StructureRegistry.create(plan.type, t, spot.x, spot.y, world); if (!structure) { - t.buildPlan = null; - return false; + return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044"); } world.addItem?.(structure, `build:${plan.type}`) || world.items.push(structure); world.drawListDirty = true; @@ -2859,9 +2897,12 @@ function protectSleepSession(tarinai, world, needs) { this.intentLockTimer = Math.max(0, (this.intentLockTimer || 0) - dt); this.aiTimer += dt; const urgentAi = this.fightTimer > 0.04 || this.intimidateTimer > 0.04 || this.intimidatedTimer > 0.04 || this.birthRitualTimer > 0.04 || this.defeatedTimer > 0.04 || this.eatTimer > 0.02 || this.hurtTimer > 0.02; - const popPerf = this.world.performanceLevel ? this.world.performanceLevel() : 0; - const aiInterval = urgentAi ? (popPerf >= 2 ? 0.105 : 0.075) : (0.18 + stableUnit(this.familyKey || this.id, "ai-interval") * 0.09 + popPerf * 0.055); - if (this.aiTimer >= aiInterval) { + const quality = this.world.performanceQuality ? this.world.performanceQuality() : {}; + const pressure = clamp(Number(quality.pressure || 0) || 0, 0, 1); + const intervalScale = Number(quality.aiIntervalScale || 1) || 1; + const phaseOffset = stableUnit(this.familyKey || this.id, "ai-phase") * 0.12 * intervalScale; + const aiInterval = urgentAi ? (0.065 + pressure * 0.045) : ((0.18 + stableUnit(this.familyKey || this.id, "ai-interval") * 0.09) * intervalScale + phaseOffset); + if (this.aiTimer >= aiInterval && this.world.spendScheduledWork?.("ai", urgentAi) !== false) { const aiDt = Math.min(this.aiTimer, 0.48); this.aiTimer = 0; this.resolveNeeds(aiDt); @@ -2878,8 +2919,8 @@ function protectSleepSession(tarinai, world, needs) { this.updateFacing(); } this.envTimer += dt; - const envInterval = (this.world.performanceLevel && this.world.performanceLevel() >= 2) ? 0.82 : 0.48; - if (this.envTimer >= envInterval) { + const envInterval = (0.48 + stableUnit(this.familyKey || this.id, "env-phase") * 0.18) * (Number(quality.envIntervalScale || 1) || 1); + if (this.envTimer >= envInterval && this.world.spendScheduledWork?.("env", urgentAi || this.world.selected === this) !== false) { const envDt = Math.min(this.envTimer, 1.2); this.envTimer = 0; this.applyEnvironment(envDt); @@ -3210,9 +3251,19 @@ function protectSleepSession(tarinai, world, needs) { const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && mealReady && wantsFood && this.hunger > 18; const canSweetBite = !this.isZunchiSlave && this.eatCooldown <= 0 && mealReady && (wantsFood || wantsMedicine) && this.hunger > 7; const canZunchiBite = this.eatCooldown <= 0 && (this.isZunchiSlave ? this.hunger > 16 : (this.hunger > 88 || foodNeed > 82)); - const foodCandidates = this.world.nearbyItems(this.x, this.y, this.radius + 46).slice(); - if (this.target && !this.target.dead && !foodCandidates.includes(this.target) && (this.target.type === "duplicator" || roleMatches(this.target, "food") || roleMatches(this.target, "medicine"))) foodCandidates.unshift(this.target); - foodCandidates.sort((a, b) => foodPriorityScore(this, b, b?.type === "duplicator" ? foodInteractionDistance(this, b) : dist(this, b)) - foodPriorityScore(this, a, a?.type === "duplicator" ? foodInteractionDistance(this, a) : dist(this, a))); + const quality = this.world?.performanceQuality?.() || {}; + const scanLimit = Math.max(3, Number(quality.itemScanLimit || 28) || 28); + const nearbyItems = this.world.nearbyItems(this.x, this.y, this.radius + 46); + const foodCandidates = []; + if (this.target && !this.target.dead && (this.target.type === "duplicator" || roleMatches(this.target, "food") || roleMatches(this.target, "medicine"))) foodCandidates.push(this.target); + for (const it of nearbyItems) { + if (!it || it.dead || foodCandidates.includes(it)) continue; + foodCandidates.push(it); + if (foodCandidates.length >= scanLimit) break; + } + if (foodCandidates.length > 1 && foodCandidates.length <= scanLimit) { + foodCandidates.sort((a, b) => foodPriorityScore(this, b, b?.type === "duplicator" ? foodInteractionDistance(this, b) : dist(this, b)) - foodPriorityScore(this, a, a?.type === "duplicator" ? foodInteractionDistance(this, a) : dist(this, a))); + } for (const it of foodCandidates) { const d = it?.type === "duplicator" ? foodInteractionDistance(this, it) : dist(this, it); const contactReach = foodInteractionReach(this, it, "food") + (it === this.target && foodActionActive && it?.type !== "duplicator" ? 4 : 0); diff --git a/js/tarinai_render.js b/js/tarinai_render.js index 023ea6c..096f34a 100644 --- a/js/tarinai_render.js +++ b/js/tarinai_render.js @@ -7,6 +7,9 @@ draw(ctx, t, lighting = null) { if (this.dead) return; const lightState = lighting || getLightingState(this.world); + const quality = this.world.performanceQuality?.() || {}; + const effectDensity = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1); + const showDecor = effectDensity > 0.45 || this.world.selected === this; const sprite = this.spriteId(); const img = typeof getRenderableImage === "function" ? getRenderableImage(sprite, "smile") : (images.get(sprite) || images.get(this.type)); const metrics = getImageMetrics(sprite) || getImageMetrics(this.type) || getImageMetrics("smile"); @@ -63,18 +66,18 @@ const shadow = projectedShadowParams(lightState, 0.95 + visualScale * 0.8); const shadowContactY = this.y + bob + (drawH / squish) * 0.30; - if (img) { + if (showDecor && img) { drawImageProjectedShadow(ctx, img, this.x, shadowContactY, drawW * squish, drawH / squish, shadow, { faceRight, alpha: shadow.alpha * (this.state === "sleep" ? 1.10 : 0.92) * nestHideAlpha, widthScale: this.state === "sleep" ? 1.12 : 1, heightScale: this.state === "sleep" ? 0.88 : 1, }); - } else { + } else if (showDecor) { drawProjectedShadow(ctx, this.x, shadowContactY, drawW * 0.52, drawH * 0.13, { ...shadow, alpha: shadow.alpha * 0.82 * nestHideAlpha }); } - drawCreatureGlow(ctx, this, drawW, drawH, lightState); - if (this.powerItemMode === "protein" || this.powerItemMode === "niteropu") { + if (showDecor) drawCreatureGlow(ctx, this, drawW, drawH, lightState); + if (showDecor && (this.powerItemMode === "protein" || this.powerItemMode === "niteropu")) { ctx.save(); const up = this.powerItemMode === "protein"; const baseColor = up ? "rgba(224,54,40,0.92)" : "rgba(116,80,190,0.88)"; @@ -194,7 +197,7 @@ ctx.textBaseline = "middle"; ctx.fillText(focusLabel, 0, ly); ctx.restore(); - } else if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && this.world.pointer?.inside && distXY(this.x, this.y, this.world.pointer.x, this.world.pointer.y) < this.radius * 2.0) { + } else if (showDecor && this.world.pointer?.inside && distXY(this.x, this.y, this.world.pointer.x, this.world.pointer.y) < this.radius * 2.0) { ctx.save(); ctx.globalAlpha = 0.28; ctx.strokeStyle = this.state === "cursor_enemy" ? "rgba(180, 92, 92, 0.70)" : "rgba(255, 250, 220, 0.78)"; @@ -231,7 +234,7 @@ ctx.restore(); } - if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && (this.goodMode === "smile" || this.state === "cursor_friend") && this.mood > 58 && this.stress < 44 && this.state !== "sleep") { + if (showDecor && (this.goodMode === "smile" || this.state === "cursor_friend") && this.mood > 58 && this.stress < 44 && this.state !== "sleep") { ctx.save(); const cursorLove = this.state === "cursor_friend" ? Math.max(0, this.cursorPetting || 0) : 0; ctx.globalAlpha = this.state === "cursor_friend" ? clamp(0.82 + cursorLove * 0.16, 0.82, 1) : 0.34; @@ -312,8 +315,10 @@ ctx.font = `${starSize}px "Apple Color Emoji", "Segoe UI Emoji", sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; - ctx.shadowColor = "rgba(255, 198, 28, 0.82)"; - ctx.shadowBlur = 14; + if (showDecor) { + ctx.shadowColor = "rgba(255, 198, 28, 0.82)"; + ctx.shadowBlur = 14; + } ctx.lineWidth = Math.max(2, starSize * 0.12); ctx.strokeStyle = "rgba(92, 62, 0, 0.86)"; ctx.fillStyle = "rgba(255, 218, 46, 1)"; @@ -324,7 +329,7 @@ const hpActive = (this.hpBarTimer || 0) > 0; const stressActive = (this.stressBarTimer || 0) > 0; - if (hpActive || stressActive) { + if ((hpActive || stressActive) && (showDecor || this.world.selected === this || this.hurtTimer > 0.04)) { const bw = Math.max(28, this.radius * 2.35); const bh = 5; const x = this.x - bw / 2; diff --git a/js/tarinai_social_move_life.js b/js/tarinai_social_move_life.js index 2eee469..5d302fc 100644 --- a/js/tarinai_social_move_life.js +++ b/js/tarinai_social_move_life.js @@ -6,8 +6,13 @@ Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ interactWithOthers(dt) { let closeCount = 0; + const quality = this.world?.performanceQuality?.() || {}; + const scanLimit = Math.max(1, Number(quality.socialScanLimit || 14) || 14); + let scanned = 0; for (const o of this.world.nearbyTarinai(this.x, this.y, 78)) { if (o === this || o.dead) continue; + if (scanned >= scanLimit && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04) break; + scanned += 1; const dx = o.x - this.x, dy = o.y - this.y; const d = Math.hypot(dx, dy); if (d < 0.01 || d > 74) continue; diff --git a/js/ui_layout_dialogs.js b/js/ui_layout_dialogs.js index 1f150f2..113acd6 100644 --- a/js/ui_layout_dialogs.js +++ b/js/ui_layout_dialogs.js @@ -2,9 +2,10 @@ function resizeCanvas() { const rect = canvas.getBoundingClientRect(); - const perf = world?.performanceLevel ? world.performanceLevel() : 0; - const maxDpr = perf >= 2 ? 1 : (perf === 1 ? 1.5 : 2); - const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, maxDpr)); + const quality = world?.performanceQuality?.() || {}; + const perf = Number(quality.level ?? (world?.performanceLevel ? world.performanceLevel() : 0)) || 0; + const maxDpr = Number(quality.maxDpr || 0) || (perf >= 2 ? 1 : (perf === 1 ? 1.25 : 2)); + const dpr = Math.max(0.6, Math.min(window.devicePixelRatio || 1, maxDpr)); const nextW = Math.max(1, Math.floor(rect.width * dpr)); const nextH = Math.max(1, Math.floor(rect.height * dpr)); if (canvas.width === nextW && canvas.height === nextH && uiCache.canvasDpr === dpr) return; diff --git a/js/world_placement_log.js b/js/world_placement_log.js index 85b1415..55589a7 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -38,7 +38,7 @@ return chars.slice(0, Math.max(0, limit)).join(""); } - function sanitizeSignboardText(value = "") { + function sanitizeSignboardText(value = "", opts = {}) { const normalized = String(value || "").replace(/\r/g, "").replace(/[\t ]+/g, " "); const rawLines = normalized.split("\n").slice(0, SIGNBOARD_LINES); const out = []; @@ -47,7 +47,8 @@ if (rawLine == null) break; out.push(sliceGraphemes(String(rawLine), SIGNBOARD_CHARS_PER_LINE)); } - return out.join("\n").replace(/\n+$/g, ""); + const next = out.join("\n"); + return opts.trimTrailing === false ? next : next.replace(/\n+$/g, ""); } function signboardVisibleLength(text = "") { @@ -95,7 +96,8 @@ const currentLine = lines[lineIndex] || ""; const replacing = selected.length > 0; if (e.key === "Enter") { - if (lines.length >= SIGNBOARD_LINES) e.preventDefault(); + const selectedLineBreaks = (selected.match(/\n/g) || []).length; + if (lines.length - selectedLineBreaks >= SIGNBOARD_LINES) e.preventDefault(); return; } if (e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { @@ -110,12 +112,12 @@ const text = e.clipboardData?.getData?.("text") || ""; const start = Number(area.selectionStart || 0); const end = Number(area.selectionEnd || start); - const next = sanitizeSignboardText((area.value || "").slice(0, start) + text + (area.value || "").slice(end)); + const next = sanitizeSignboardText((area.value || "").slice(0, start) + text + (area.value || "").slice(end), { trimTrailing: false }); area.value = next; syncCount(); }; area.oninput = () => { - const clean = sanitizeSignboardText(area.value || ""); + const clean = sanitizeSignboardText(area.value || "", { trimTrailing: false }); if (area.value !== clean) area.value = clean; syncCount(); }; diff --git a/js/world_spatial_budget.js b/js/world_spatial_budget.js index 216b318..fde3219 100644 --- a/js/world_spatial_budget.js +++ b/js/world_spatial_budget.js @@ -12,7 +12,7 @@ Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ performanceLevel() { - if (window.TarinaiPerformance) return window.TarinaiPerformance.level || 0; + 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; @@ -25,12 +25,51 @@ return window.TarinaiPerformance?.quality?.(this) || { level: this.performanceLevel(), antStride: 1, effectLimit: CONFIG.effectLimit ?? 96 }; }, + beginFramePerformanceBudgets(quality = this.performanceQuality?.()) { + const pressure = clamp(Number(quality?.pressure || 0) || 0, 0, 1); + const density = clamp(Number(quality?.effectDensity || (1 - pressure)) || 0, 0.12, 1); + this.workBudget = { + ai: Math.max(4, Math.round(4 + density * 24)), + env: Math.max(3, Math.round(3 + density * 15)), + aiUsed: 0, + envUsed: 0, + }; + this.workStats = { + aiRuns: 0, + aiSkips: 0, + envRuns: 0, + envSkips: 0, + collisionRuns: 0, + collisionSkips: 0, + bedConflictRuns: 0, + bedConflictSkips: 0, + }; + }, + + spendScheduledWork(kind = "ai", urgent = false) { + if (!this.workBudget) this.beginFramePerformanceBudgets?.(); + const stats = this.workStats || {}; + const budget = this.workBudget || {}; + const usedKey = `${kind}Used`; + const runKey = `${kind}Runs`; + const skipKey = `${kind}Skips`; + if (urgent || (budget[usedKey] || 0) < (budget[kind] || 0)) { + budget[usedKey] = (budget[usedKey] || 0) + 1; + stats[runKey] = (stats[runKey] || 0) + 1; + return true; + } + stats[skipKey] = (stats[skipKey] || 0) + 1; + return false; + }, + runtimeDiagnostics() { const behaviorCounts = {}; let forcedBehaviors = 0; let lockedBehaviors = 0; + let liveTarinai = 0; for (const t of this.tarinai || []) { if (!t || t.dead) continue; + liveTarinai += 1; const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(t) : t.behavior; const id = behavior?.id || t.state || "none"; behaviorCounts[id] = (behaviorCounts[id] || 0) + 1; @@ -65,6 +104,18 @@ locked: lockedBehaviors, counts: behaviorCounts, }, + performance: { + liveTarinai, + fps: window.TarinaiPerformance?.fps || window.__tarinaiFps || 0, + pressure: this.performanceQuality?.().pressure || 0, + level: this.performanceLevel?.() || 0, + dpr: (typeof uiCache !== "undefined" ? uiCache?.canvasDpr : window.uiCache?.canvasDpr) || 0, + quality: this.performanceQuality?.() || null, + }, + work: { + budget: this.workBudget || null, + stats: this.workStats || null, + }, }; }, diff --git a/js/world_update.js b/js/world_update.js index 0a41d9e..a6e8a3b 100644 --- a/js/world_update.js +++ b/js/world_update.js @@ -3,6 +3,32 @@ (function (global) { const World = global.World; if (!World) throw new Error("World is not available for mixin: world_update.js"); + function markSpatialDirtyIfMoved(worldRef, list, reason = "moved", threshold = 0.35) { + if (!worldRef || !Array.isArray(list) || !list.length) return false; + const thresholdSq = threshold * threshold; + let moved = false; + for (const entity of list) { + if (!entity || entity.dead) continue; + const x = Number.isFinite(entity.x) ? entity.x : 0; + const y = Number.isFinite(entity.y) ? entity.y : 0; + const oldX = entity._spatialTrackX; + const oldY = entity._spatialTrackY; + if (!Number.isFinite(oldX) || !Number.isFinite(oldY)) { + entity._spatialTrackX = x; + entity._spatialTrackY = y; + moved = true; + continue; + } + const dx = x - oldX; + const dy = y - oldY; + if (dx * dx + dy * dy <= thresholdSq) continue; + entity._spatialTrackX = x; + entity._spatialTrackY = y; + moved = true; + } + if (moved) worldRef.markSpatialDirty?.(reason); + return moved; + } Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ environmentScores() { const alive = this.tarinai || []; @@ -227,29 +253,47 @@ this.spatialDirtyMarksThisFrame = 0; this.ensureSpatial?.("update-start"); - const perf = this.performanceLevel(); + const quality = this.performanceQuality?.() || {}; + const perf = Number(quality.level ?? this.performanceLevel?.() ?? 0) || 0; + this.beginFramePerformanceBudgets?.(quality); const itemCountBefore = this.items.length; for (const it of this.items) it.update(dt, this); - if (this.items.length) this.markSpatialDirty?.("items-updated"); + const itemsMoved = markSpatialDirtyIfMoved(this, this.items, "items-moved", 0.25); if (this.itemCounts?.ball) { this.ensureSpatial?.("ball-ball-collisions"); this.resolveBallBallCollisions(dt); this.markSpatialDirty?.("ball-ball-collisions"); } this.updateAnts?.(dt); - if ((this.ants || []).length) this.markSpatialDirty?.("ants-updated"); + const antsMoved = markSpatialDirtyIfMoved(this, this.ants || [], "ants-moved", 0.25); + if (itemsMoved || antsMoved || this.spatialDirty) this.ensureSpatial?.("post-mobile-item-update"); for (const ef of this.effects) ef.update(dt); for (const t of this.tarinai) { t.update(dt); } - this.resolveOwnedBedSleepConflicts?.(); - if (this.tarinai.length) this.markSpatialDirty?.("tarinai-updated"); + const now = this.time || 0; + const bedConflictInterval = Number(quality.bedConflictInterval || 0.12) || 0.12; + if (now >= (this.nextBedConflictCheckAt || 0)) { + this.nextBedConflictCheckAt = now + bedConflictInterval; + this.workStats && (this.workStats.bedConflictRuns = (this.workStats.bedConflictRuns || 0) + 1); + this.resolveOwnedBedSleepConflicts?.(); + } else { + this.workStats && (this.workStats.bedConflictSkips = (this.workStats.bedConflictSkips || 0) + 1); + } + markSpatialDirtyIfMoved(this, this.tarinai, "tarinai-moved", 0.25); this.ensureSpatial?.("post-tarinai-update"); this.limitBallChasers(); this.resolveBallInteractions(dt); - this.resolveTarinaiHighSpeedCollisions(dt); + const collisionInterval = Number(quality.collisionInterval || 0.04) || 0.04; + if (now >= (this.nextTarinaiCollisionCheckAt || 0)) { + this.nextTarinaiCollisionCheckAt = now + collisionInterval; + this.workStats && (this.workStats.collisionRuns = (this.workStats.collisionRuns || 0) + 1); + this.resolveTarinaiHighSpeedCollisions(dt); + } else { + this.workStats && (this.workStats.collisionSkips = (this.workStats.collisionSkips || 0) + 1); + } this.compactTimer += dt; const compactInterval = perf >= 2 ? 0.95 : 0.58; diff --git a/scripts/__pycache__/regression_check.cpython-313.pyc b/scripts/__pycache__/regression_check.cpython-313.pyc deleted file mode 100644 index e191a0c..0000000 Binary files a/scripts/__pycache__/regression_check.cpython-313.pyc and /dev/null differ