"use strict"; (function () { const params = new URLSearchParams(location.search || ""); const enabled = params.get("debug") === "1" || params.get("debug") === "true"; window.__tarinaiDebugOverlayEnabled = enabled; if (!enabled) return; const host = document.createElement("div"); host.id = "debugOverlay"; host.style.cssText = [ "position:fixed", "left:8px", "top:76px", "z-index:99999", "width:min(620px, calc(100vw - 16px))", "max-height:calc(100vh - 92px)", "overflow:hidden", "font:11px/1.42 ui-monospace,SFMono-Regular,Menlo,monospace", "color:#f7fbff", "background:rgba(18,24,30,.86)", "border:1px solid rgba(255,255,255,.18)", "border-radius:10px", "pointer-events:auto", "box-shadow:0 8px 24px rgba(0,0,0,.22)" ].join(";"); const bar = document.createElement("div"); bar.style.cssText = [ "display:flex", "gap:6px", "align-items:center", "padding:6px 8px", "border-bottom:1px solid rgba(255,255,255,.14)", "background:rgba(255,255,255,.06)", "user-select:none", "-webkit-user-select:none" ].join(";"); const title = document.createElement("span"); title.style.cssText = "flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"; const copyBtn = document.createElement("button"); copyBtn.type = "button"; copyBtn.textContent = "Copy"; const minBtn = document.createElement("button"); minBtn.type = "button"; minBtn.textContent = "Min"; for (const btn of [copyBtn, minBtn]) { btn.style.cssText = [ "font:11px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace", "color:#f7fbff", "background:rgba(255,255,255,.12)", "border:1px solid rgba(255,255,255,.22)", "border-radius:6px", "padding:3px 7px", "cursor:pointer" ].join(";"); } bar.append(title, copyBtn, minBtn); const body = document.createElement("pre"); body.style.cssText = [ "margin:0", "padding:8px 10px", "max-height:calc(100vh - 128px)", "overflow:auto", "white-space:pre-wrap", "user-select:text", "-webkit-user-select:text", "cursor:text" ].join(";"); body.tabIndex = 0; host.append(bar, body); document.addEventListener("DOMContentLoaded", () => document.body.appendChild(host)); let copiedMessageUntil = 0; let minimized = false; let lastText = ""; function selectBodyText() { const range = document.createRange(); range.selectNodeContents(body); const selection = window.getSelection?.(); if (selection) { selection.removeAllRanges(); selection.addRange(range); } } copyBtn.addEventListener("click", async (ev) => { ev.preventDefault(); const text = lastText || body.textContent || ""; try { await navigator.clipboard?.writeText?.(text); copiedMessageUntil = Date.now() + 1400; copyBtn.textContent = "Copied"; } catch (_) { selectBodyText(); copiedMessageUntil = Date.now() + 1400; copyBtn.textContent = "Select"; } }); minBtn.addEventListener("click", (ev) => { ev.preventDefault(); minimized = !minimized; body.style.display = minimized ? "none" : "block"; host.style.width = minimized ? "min(360px, calc(100vw - 16px))" : "min(620px, calc(100vw - 16px))"; minBtn.textContent = minimized ? "Open" : "Min"; }); let lastEvents = []; let lastAudio = []; window.TarinaiEvents.on("*", ev => { lastEvents.unshift(ev.type); if (lastEvents.length > 10) lastEvents.length = 10; }); window.TarinaiEvents.on("audio:play", ev => { lastAudio.unshift(`${ev.detail?.id || "?"}:${ev.detail?.category || "?"}`); if (lastAudio.length > 8) lastAudio.length = 8; }); function fmtMs(v) { const n = Number(v || 0); return n >= 10 ? n.toFixed(1) : n.toFixed(2); } function shortLabel(label) { return String(label || "?") .replace(/^update\.phase\./, "phase.") .replace(/^update\./, "u.") .replace(/^render\./, "r.") .replace(/^frame\./, "f."); } function topMap(map, limit = 4) { return Object.entries(map || {}).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([k, v]) => `${k}:${v}`).join(", ") || "-"; } function saveStatsLines() { const codec = window.TarinaiSaveCodec; const stats = codec?.getLastEncodeStats?.(); if (!stats) return []; const load = codec?.getLastDecodeStats?.(); const sections = stats.sections || {}; const details = stats.details || {}; const rawTotal = Math.max(1, Number(stats.rawBinaryBytes || stats.binaryBytes) || 0); const part = key => { const bytes = Number(sections[key]) || 0; return `${bytes}B/${Math.round(bytes / rawTotal * 100)}%`; }; const ageSeconds = Math.max(0, Math.round((Date.now() - (Number(stats.createdAt) || Date.now())) / 1000)); const decode = load ? ` decode:${fmtMs(load.decodeMs)}ms` : ""; return [ `save stats ${stats.hashChars || 0} chars stored:${stats.binaryBytes || 0}B raw:${stats.rawBinaryBytes || stats.binaryBytes || 0}B ${stats.compressed ? "deflate" : "plain"} encode:${fmtMs(stats.encodeMs)}ms${decode} age:${ageSeconds}s`, ` save format:${part("format")} strings:${part("stringTable")}(${details.stringTableEntries || 0}) world:${part("world")}`, ` save tarinai basic:${part("tarinaiBasic")} seed:${part("tarinaiSeed")} rel:${part("tarinaiRelationships")} count:${details.tarinaiCount || 0} relRows:${details.relationshipRows || 0} parents:${details.parentLinks || 0}`, ` save items index:${part("itemIndex")} normal:${part("normalItems")}(${details.normalItemCount || 0}) physics:${part("physicsItems")}(${details.physicsItemCount || 0})`, ` save extra normal:${details.normalItemExtraEncodedBytes || 0}B(raw ${details.normalItemExtraRawUtf8Bytes || 0}B) physics:${details.physicsItemExtraEncodedBytes || 0}B(raw ${details.physicsItemExtraRawUtf8Bytes || 0}B) seedRaw:${details.birthSeedRawUtf8Bytes || 0}B`, ` save accounted:${stats.accountedBytes || 0}B unaccounted:${stats.unaccountedBytes || 0}B compressed:${stats.compressedBytes || 0}B saved:${stats.compressionSavedBytes || 0}B text:${stats.textBits || 15}bit payload:${stats.payloadChars || 0} chars`, ]; } function perfLines(perf) { const self = (perf.topSelf || perf.entries || []).filter(e => e.label !== "frame.total").slice(0, 10); const incl = (perf.topInclusive || perf.entries || []).filter(e => e.label !== "frame.total").slice(0, 6); const selfLines = self.map((e, i) => `${String(i + 1).padStart(2, " ")}. ${shortLabel(e.label).padEnd(28, " ")} self ${fmtMs(e.selfAvg)}ms total ${fmtMs(e.avg)}ms ${e.selfPct || 0}%`); const inclLine = incl.map(e => `${shortLabel(e.label)}:${fmtMs(e.avg)}`).join(" "); return [ `heavy self avg`, ...(selfLines.length ? selfLines : [" -"]), `heavy inclusive ${inclLine || "-"}`, ]; } function diagnosticHints(diag, perf) { const hints = []; const entries = perf.topSelf || perf.entries || []; const top = entries[0]; if (top && top.selfAvg >= 4) hints.push(`hot:${shortLabel(top.label)} ${fmtMs(top.selfAvg)}ms self`); if ((diag.spatial?.rebuildsThisFrame || 0) > 1) hints.push(`spatial rebuild/frame ${diag.spatial.rebuildsThisFrame}`); if ((diag.creatures?.smooth || 0) > 0) hints.push(`smooth motion ${diag.creatures.smooth}`); if ((diag.scheduler?.activeRealtime || 0) > 120) hints.push(`realtime items ${diag.scheduler.activeRealtime}`); if ((diag.scheduler?.heap || 0) > 700) hints.push(`scheduled heap ${diag.scheduler.heap}`); if ((diag.effects?.skipped || 0) > 0) hints.push(`effect update skip ${diag.effects.skipped}`); if ((diag.effectRender?.skipped || 0) > 0) hints.push(`effect draw skip ${diag.effectRender.skipped}`); if ((diag.creatures?.fullBudgetSkips || 0) > 0) hints.push(`creature bg skip ${diag.creatures.fullBudgetSkips}`); if ((diag.items?.bucketRebuildsTotal || 0) > 0 && diag.items?.bucketsDirty) hints.push("item buckets dirty"); const m = diag.scheduler?.mechanicalStats || diag.items?.mechanicalStats || diag.scheduler?.physics?.mechanical || null; if (m?.candidates > 250 || m?.pairs > 180) hints.push(`mechanical candidates ${m.candidates || "?"} pairs ${m.pairs || m.solved || "?"}`); const c = diag.scheduler?.constraintStats || null; if (c?.totalLinks > 80) hints.push(`constraints links ${c.totalLinks} ran ${c.ran}`); return hints.join(" ") || "-"; } const DEBUG_REFRESH_MS = 750; window.setInterval(() => { const w = window.world; const input = window.TarinaiInputMode.snapshot() || {}; const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {}; const behaviorCounts = diag.behavior?.counts || {}; const topBehaviors = topMap(behaviorCounts, 5); const perf = diag.perf || window.TarinaiPerf.snapshot() || {}; const metrics = perf.metrics || {}; const visual = perf.visual || {}; const currentDpr = (typeof uiCache !== "undefined" && uiCache) ? uiCache.canvasDpr : "?"; const creature = diag.creatures || {}; const scheduler = diag.scheduler || {}; const effects = diag.effects || {}; const effectRender = diag.effectRender || {}; const terrain = diag.terrain || {}; const lines = [ `tarinai v${window.TARINAI_VERSION || "?"} debug profiler:${perf.enabled ? "on" : "off"}${Date.now() < copiedMessageUntil ? " copied" : " copy:button/select"}`, `fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms profile:${perf.profile || "?"} visual effects:${visual.effects || "?"} shadows:${visual.shadows || "?"} details:${visual.details || perf.tier || "?"} dprScale:${perf.dprScale || 1}`, `live ${diag.runtime?.liveTarinai ?? (w?.tarinai || []).length} items ${(w?.items || []).length} ants ${(w?.ants || []).length} effects ${(w?.effects || []).length}`, ...saveStatsLines(), `visible layered:${metrics["render.visibleLayered"] ?? "?"} back:${metrics["render.visibleBackItems"] ?? "?"} effects:${metrics["render.visibleEffects"] ?? "?"}/${effectRender.budget ?? metrics["render.effectDrawBudget"] ?? "?"} skip:${effectRender.skipped ?? metrics["render.effectDrawSkipped"] ?? "?"}`, `creatures full:${creature.full ?? 0} bg:${creature.fullBudgetUsed ?? 0}/${creature.fullBudget ?? "?"} bgSkip:${creature.fullBudgetSkips ?? 0} vFull:${creature.visibleFullUsed ?? 0}/${creature.visibleFullBudget ?? "?"} vSkip:${creature.visibleFullSkips ?? 0} vMotion:${creature.visibleMotionUsed ?? 0}/${creature.visibleMotionBudget ?? "?"} mPressure:${creature.mechanicalPressure ?? 0}${creature.mechanicalPressureActive ? "!" : ""} mContact:${creature.mechanicalDetailedContacts ?? 0}/${creature.mechanicalContactBudget ?? "?"}+${creature.mechanicalDegradedContacts ?? 0} offSkip:${creature.offscreenSkipped ?? 0} realtime:${creature.realtime ?? 0} smooth:${creature.smooth ?? 0} skipped:${creature.skipped ?? 0} selected:${creature.selected ?? 0} urgent:${creature.urgent ?? 0} visible:${creature.visible ?? 0} sleepPhys:${creature.sleepPhysical ?? 0} passive:${creature.passivePhysical ?? 0}`, `creature lanes ${topMap(creature.lanes, 6)} smoothStates ${topMap(creature.smoothStates, 4)}`, `spatial rebuild ${diag.spatial?.rebuildsThisFrame ?? 0}/frame total:${diag.spatial?.rebuildsTotal ?? 0} partial:${diag.spatial?.partialRebuildsTotal ?? 0} dirty:${diag.spatial?.dirtyMarksThisFrame ?? 0} deferred:${diag.spatial?.deferredReads ?? 0} reason:${diag.spatial?.dirtyReason || diag.spatial?.lastRebuildReason || "-"}`, `spatial dirtyReasons ${topMap(diag.spatial?.dirtyReasons, 4)} rebuildReasons ${topMap(diag.spatial?.rebuildReasons, 4)}`, `nearby obstacleRects hit:${diag.nearby?.obstacleRectHits ?? 0} miss:${diag.nearby?.obstacleRectMisses ?? 0} built:${diag.nearby?.obstacleRectBuilt ?? 0} bypass:${diag.nearby?.obstacleRectBypass ?? 0} constraintDirty mark:${diag.constraintDirty?.marked ?? 0} coal:${diag.constraintDirty?.coalesced ?? 0}`, `scheduler heap:${scheduler.heap ?? 0} realtime:${scheduler.activeRealtime ?? scheduler.realtime ?? 0} ran:${scheduler.lastRan ?? 0} itemRt:${scheduler.realtimeRan ?? 0} itemDue:${scheduler.dueRan ?? 0} constraints:${scheduler.constraints ?? 0} postPairs:${scheduler.postConstraintPairs ?? 0} rodStretch:${fmtMs(scheduler.constraintStats?.rodStretchMax || 0)} corr:${scheduler.constraintStats?.rodCorrections ?? 0}`, `items active:${scheduler.activeRealtime ?? 0} asleep:${scheduler.sleeping ?? scheduler.heap ?? 0} budgetPairs:${scheduler.physicsBudget?.maxPairs ?? "?"} substeps:${scheduler.physics?.mechanical?.substeps ?? scheduler.mechanicalStats?.substeps ?? "?"} fenceCCD:${Boolean((scheduler.physics?.mechanical || scheduler.mechanicalStats)?.fenceSensitive)}`, `effects update:${effects.updated ?? 0}/${effects.budget ?? effects.total ?? 0} skipped:${effects.skipped ?? 0} compact:${effects.compacted ?? 0} dropped:${effects.dropped ?? 0} render:${effectRender.drawn ?? 0}/${effectRender.budget ?? "?"} rSkip:${effectRender.skipped ?? 0} level:${effectRender.level || visual.effects || "?"}`, `terrain dirty raw:${terrain.raw ?? 0} global:${terrain.global ?? 0} chunk:${terrain.chunk ?? 0} coalesced:${terrain.coalesced ?? 0} reasons:${topMap(terrain.reasons, 4)}`, `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}`, `hints ${diagnosticHints(diag, perf)}`, ...perfLines(perf), `behavior forced:${diag.behavior?.forced ?? 0} locked:${diag.behavior?.locked ?? 0} top ${topBehaviors || "-"}`, `terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0} drawList ${(w?.drawList || []).length} dirty:${Boolean(diag.render?.drawListDirty)}`, `item buckets:${diag.items?.bucketTypes ?? 0} dirty:${Boolean(diag.items?.bucketsDirty)} id-map:${diag.items?.idMapSize ?? 0} bucketRebuilds:${diag.items?.bucketRebuildsTotal ?? 0}`, `dpr ${currentDpr} cache ${window.TARINAI_APP?.cacheName || "?"} sw ${navigator.serviceWorker?.controller ? "controlled" : "uncontrolled"}`, `input ${input.currentMode || "?"} touchFirst:${Boolean(input.touchFirst)} coarse:${Boolean(input.pointerCoarse)} hover:${Boolean(input.hasHover)} compact:${Boolean(input.isCompactViewport)}`, `audio ${lastAudio.join(", ") || window.TarinaiAudio.lastPlayedId || "-"}`, `events ${lastEvents.join(", ")}`, ]; lastText = lines.join("\n"); title.textContent = minimized ? `tarinai v${window.TARINAI_VERSION || "?"} fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms` : `tarinai debug fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms`; if (Date.now() >= copiedMessageUntil && copyBtn.textContent !== "Copy") copyBtn.textContent = "Copy"; if (!minimized) body.textContent = lastText; }, DEBUG_REFRESH_MS); })();