"use strict"; (function (global) { const nowMs = () => (global.performance?.now ? global.performance.now() : Date.now()); const debugEnabled = (() => { try { const params = new URLSearchParams(global.location?.search || ""); return params.get("debug") === "1" || params.get("debug") === "true" || params.get("perf") === "1"; } catch (_) { return false; } })(); const buckets = new Map(); const stack = []; const metrics = new Map(); let frameOpen = null; let frameCount = 0; let lastSmoothAt = 0; const storageKey = "tarinai_visual_settings_v2"; const legacyStorageKey = "tarinai_visual_settings_v1"; const allowed = { effects: new Set(["off", "high"]), shadows: new Set(["off", "high"]), details: new Set(["auto", "low", "high"]), }; const defaults = Object.freeze({ effects: "high", shadows: "high", details: "auto" }); const PROFILE_FULL = Object.freeze({ id: "full", renderTier: "high", dprScale: 1, simulationThrottled: false, creatureCadenceScale: 0, creatureBackgroundBudget: 1000000000, pressureCreatureFullBudget: 1000000000, pressureCreatureMotionBudget: 1000000000, pressureObstacleContactBudget: 1000000000, collisionSourceBudget: 1000000000, collisionImpactBudget: 1000000000, itemCadenceScale: 0, scheduledItemBudget: 1000000000, aiBudget: 1000000000, envBudget: 1000000000, effectCap: 1000000000, effectUpdateBudget: 1000000000, effectDrawBudget: 160, fireInteractionBudget: 1000000000, activeFireLimit: 1000000000, physicsBudget: Object.freeze({ maxItems: 1000000000, maxPairs: 1000000000, maxLinks: 1000000000, postMaxPairs: 1000000000, maxSubsteps: 8, focusThreshold: 1000000000, maxSubstepPairs: 1000000000, maxSubstepFenceItems: 1000000000, }), poisonContactBudget: Object.freeze({ maxPoison: 1000000000, maxTargets: 1000000000, skip: false }), }); const PROFILE_BALANCED = Object.freeze({ id: "balanced", renderTier: "mid", dprScale: 0.86, simulationThrottled: true, creatureCadenceScale: 1, creatureBackgroundBudget: 36, pressureCreatureFullBudget: 36, pressureCreatureMotionBudget: 58, pressureObstacleContactBudget: 14, collisionSourceBudget: 36, collisionImpactBudget: 20, itemCadenceScale: 1, scheduledItemBudget: 30, aiBudget: 10, envBudget: 5, effectCap: 220, effectUpdateBudget: 84, effectDrawBudget: 96, fireInteractionBudget: 14, activeFireLimit: 60, physicsBudget: Object.freeze({ maxItems: 180, maxPairs: 170, maxLinks: 120, postMaxPairs: 48, maxSubsteps: 3, focusThreshold: 38, maxSubstepPairs: 96, maxSubstepFenceItems: 24, }), poisonContactBudget: Object.freeze({ maxPoison: 12, maxTargets: 42, skip: false }), }); const PROFILE_AGGRESSIVE = Object.freeze({ id: "aggressive", renderTier: "low", dprScale: 0.72, simulationThrottled: true, creatureCadenceScale: 1.55, creatureBackgroundBudget: 16, pressureCreatureFullBudget: 18, pressureCreatureMotionBudget: 30, pressureObstacleContactBudget: 7, collisionSourceBudget: 18, collisionImpactBudget: 10, itemCadenceScale: 1.55, scheduledItemBudget: 14, aiBudget: 6, envBudget: 3, effectCap: 140, effectUpdateBudget: 48, effectDrawBudget: 36, fireInteractionBudget: 8, activeFireLimit: 40, physicsBudget: Object.freeze({ maxItems: 100, maxPairs: 82, maxLinks: 52, postMaxPairs: 20, maxSubsteps: 1, focusThreshold: 28, maxSubstepPairs: 54, maxSubstepFenceItems: 14, }), poisonContactBudget: Object.freeze({ maxPoison: 8, maxTargets: 28, skip: false }), }); const profiles = Object.freeze({ full: PROFILE_FULL, balanced: PROFILE_BALANCED, aggressive: PROFILE_AGGRESSIVE }); let visualSettings = loadVisualSettings(); let resizeRequested = false; let autoProfileId = "full"; let fpsAccumSeconds = 0; let fpsAccumFrames = 0; let smoothedFps = 0; let pendingProfileId = ""; let pendingProfileSamples = 0; function bucketFor(label) { let b = buckets.get(label); if (!b) { b = { label, ms: 0, selfMs: 0, avg: 0, selfAvg: 0, max: 0, calls: 0, last: 0, lastSelf: 0 }; buckets.set(label, b); } return b; } function begin(label) { if (!debugEnabled) return null; const frame = { label: String(label || "unknown"), start: nowMs(), childMs: 0 }; stack.push(frame); return () => { const top = stack.pop(); const active = top === frame ? frame : top || frame; if (top !== frame) { const idx = stack.lastIndexOf(frame); if (idx >= 0) stack.splice(idx, 1); } const elapsed = Math.max(0, nowMs() - active.start); const selfElapsed = Math.max(0, elapsed - (active.childMs || 0)); if (stack.length) stack[stack.length - 1].childMs += elapsed; const b = bucketFor(active.label); b.ms += elapsed; b.selfMs += selfElapsed; b.calls += 1; b.last = elapsed; b.lastSelf = selfElapsed; if (elapsed > b.max) b.max = elapsed; return elapsed; }; } function beginFrame() { frameOpen = nowMs(); if (debugEnabled) stack.length = 0; return frameOpen; } function smoothBuckets() { if (!debugEnabled) return; for (const b of buckets.values()) { b.avg = b.avg ? b.avg * 0.82 + b.ms * 0.18 : b.ms; b.selfAvg = b.selfAvg ? b.selfAvg * 0.82 + b.selfMs * 0.18 : b.selfMs; b.ms = 0; b.selfMs = 0; b.calls = 0; } } function normalizeVisualSetting(key, value) { const raw = String(value || ""); if (key === "effects" || key === "shadows") { if (raw === "off") return "off"; if (raw === "on" || raw === "low" || raw === "mid" || raw === "high") return "high"; } if (key === "details") { if (raw === "auto" || raw === "low" || raw === "high") return raw; if (raw === "mid") return "auto"; } return defaults[key]; } function sanitizeVisualSettings(value) { const src = value && typeof value === "object" ? value : {}; const next = { ...defaults }; for (const key of Object.keys(defaults)) { const normalized = normalizeVisualSetting(key, src[key]); if (allowed[key]?.has(normalized)) next[key] = normalized; } return next; } function loadVisualSettings() { try { const current = global.localStorage?.getItem(storageKey); if (current) return sanitizeVisualSettings(JSON.parse(current)); const legacy = global.localStorage?.getItem(legacyStorageKey); if (!legacy) return { ...defaults }; const migrated = sanitizeVisualSettings(JSON.parse(legacy)); // Old builds had no automatic mode. Preserve explicit effect/shadow choices, // but use the new automatic drawing mode as the migration default. migrated.details = "auto"; return migrated; } catch (_) { return { ...defaults }; } } function saveVisualSettings() { try { global.localStorage?.setItem(storageKey, JSON.stringify(visualSettings)); } catch (_) {} } function activeProfile() { if (visualSettings.details === "high") return PROFILE_FULL; if (visualSettings.details === "low") return PROFILE_AGGRESSIVE; return profiles[autoProfileId] || PROFILE_BALANCED; } function chooseAutoProfile(fps) { if (!Number.isFinite(fps) || fps <= 0) return autoProfileId; if (autoProfileId === "full") return fps < 51 ? "balanced" : "full"; if (autoProfileId === "aggressive") return fps >= 45 ? "balanced" : "aggressive"; if (fps >= 57) return "full"; if (fps < 38) return "aggressive"; return "balanced"; } function applyAutoProfile(nextId) { if (!profiles[nextId] || nextId === autoProfileId) return false; const before = activeProfile(); autoProfileId = nextId; const after = activeProfile(); if (before.dprScale !== after.dprScale || before.renderTier !== after.renderTier) resizeRequested = true; if (after.id === "aggressive") global.TarinaiAssetsMemory?.trim?.("aggressive"); try { global.dispatchEvent?.(new CustomEvent("tarinai-performance-profile", { detail: { profile: after.id, fps: smoothedFps } })); } catch (_) {} return true; } function observeFrameDelta(rawDt) { const dt = Number(rawDt); if (!Number.isFinite(dt) || dt <= 0 || dt > 0.25 || global.document?.hidden) return; fpsAccumSeconds += dt; fpsAccumFrames += 1; if (fpsAccumSeconds < 0.75) return; const sampledFps = fpsAccumFrames / Math.max(0.001, fpsAccumSeconds); smoothedFps = smoothedFps > 0 ? smoothedFps * 0.65 + sampledFps * 0.35 : sampledFps; global.__tarinaiFps = Math.round(smoothedFps); fpsAccumSeconds = 0; fpsAccumFrames = 0; if (visualSettings.details !== "auto") return; const desired = chooseAutoProfile(sampledFps); if (desired === autoProfileId) { pendingProfileId = ""; pendingProfileSamples = 0; return; } if (pendingProfileId !== desired) { pendingProfileId = desired; pendingProfileSamples = 1; return; } pendingProfileSamples += 1; if (pendingProfileSamples >= 2) { applyAutoProfile(desired); pendingProfileId = ""; pendingProfileSamples = 0; } } function endFrame() { frameCount += 1; if (frameOpen != null && debugEnabled) { const elapsed = Math.max(0, nowMs() - frameOpen); const b = bucketFor("frame.total"); b.ms += elapsed; b.selfMs += Math.max(0, elapsed - stack.reduce((sum, f) => sum + (f.childMs || 0), 0)); b.calls += 1; b.last = elapsed; b.lastSelf = b.last; if (elapsed > b.max) b.max = elapsed; } frameOpen = null; if (debugEnabled) stack.length = 0; const t = nowMs(); if (t - lastSmoothAt >= 750) { lastSmoothAt = t; smoothBuckets(); } } function visualLevel(key, fallback = "high") { if (key === "details") return activeProfile().renderTier; if (key === "effects") { if (visualSettings.effects === "off") return "off"; return activeProfile().renderTier; } if (key === "shadows") return shadowQualityTier(); return visualSettings[key] || fallback; } function visualSettingsValue() { return { ...visualSettings }; } function setVisualSetting(key, value) { if (!Object.prototype.hasOwnProperty.call(defaults, key)) return false; const nextValue = normalizeVisualSetting(key, value); if (!allowed[key]?.has(nextValue)) return false; if (visualSettings[key] === nextValue) return true; const before = activeProfile(); visualSettings = { ...visualSettings, [key]: nextValue }; saveVisualSettings(); const after = activeProfile(); if (key === "details" || before.dprScale !== after.dprScale || before.renderTier !== after.renderTier) resizeRequested = true; if (after.id === "aggressive") global.TarinaiAssetsMemory?.trim?.("aggressive"); return true; } function renderQualityTier() { return activeProfile().renderTier; } function shadowQualityTier() { if (visualSettings.shadows === "off") return "off"; return activeProfile().renderTier; } function dprScaleValue() { return activeProfile().dprScale; } function performanceProfile() { return activeProfile(); } function simulationOptimizationTier() { return activeProfile().id; } function consumeResizeRequest() { const v = resizeRequested; resizeRequested = false; return v; } function setMetric(name, value) { if (!debugEnabled) return; metrics.set(String(name || "metric"), value); } function snapshot() { const frameAvg = buckets.get("frame.total")?.avg || 0; const entries = [...buckets.values()].map(b => ({ label: b.label, avg: Number((b.avg || 0).toFixed(2)), selfAvg: Number((b.selfAvg || 0).toFixed(2)), max: Number((b.max || 0).toFixed(2)), last: Number((b.last || 0).toFixed(2)), lastSelf: Number((b.lastSelf || 0).toFixed(2)), calls: b.calls || 0, pct: frameAvg > 0 ? Number((((b.avg || 0) / frameAvg) * 100).toFixed(0)) : 0, selfPct: frameAvg > 0 ? Number((((b.selfAvg || 0) / frameAvg) * 100).toFixed(0)) : 0, })); const topInclusive = entries.slice().sort((a, b) => b.avg - a.avg).slice(0, 12); const topSelf = entries.slice().sort((a, b) => b.selfAvg - a.selfAvg).slice(0, 12); const profile = activeProfile(); return { enabled: debugEnabled, tier: profile.renderTier, profile: profile.id, autoProfile: autoProfileId, fps: Number(smoothedFps.toFixed(1)), visual: visualSettingsValue(), dprScale: dprScaleValue(), frameCount, frameAvg: Number(frameAvg.toFixed(2)), entries: topInclusive, topInclusive, topSelf, metrics: Object.fromEntries(metrics.entries()), }; } global.TarinaiPerf = Object.freeze({ begin, beginFrame, endFrame, observeFrameDelta, renderQualityTier, shadowQualityTier, visualLevel, visualSettings: visualSettingsValue, setVisualSetting, dprScale: dprScaleValue, performanceProfile, simulationOptimizationTier, consumeResizeRequest, setMetric, snapshot, }); })(typeof window !== "undefined" ? window : globalThis);