diff --git a/index.html b/index.html
index 6e51dd4..fb376a1 100644
--- a/index.html
+++ b/index.html
@@ -22,13 +22,56 @@
position: fixed; inset: 0; z-index: 100;
display: flex; align-items: center; justify-content: center;
background: #000;
- color: #00ff41; font-size: 14px; letter-spacing: 4px;
- text-shadow: 0 0 12px #00ff41;
font-family: 'Courier New', monospace;
}
#loading-screen.hidden { display: none; }
- @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
- #loading-screen span { animation: blink 1.2s ease-in-out infinite; }
+
+ .loading-content {
+ display: flex; flex-direction: column; align-items: center; gap: 18px;
+ padding: 24px;
+ }
+
+ /* ASCII logo */
+ #ascii-logo {
+ white-space: pre;
+ font-family: 'Courier New', monospace;
+ font-size: clamp(6px, 1.1vw, 13px);
+ line-height: 1.25;
+ color: #002200;
+ text-shadow: none;
+ user-select: none;
+ }
+ /* Lit characters glow green */
+ #ascii-logo .ac { color: #002200; transition: color 0.15s, text-shadow 0.15s; }
+ #ascii-logo .ac.lit {
+ color: #00ff41;
+ text-shadow: 0 0 8px #00ff41, 0 0 20px #00cc33;
+ }
+
+ /* Progress bar */
+ #loading-bar-track {
+ width: clamp(220px, 50vw, 500px);
+ height: 3px;
+ background: #001800;
+ border: 1px solid #003300;
+ border-radius: 2px;
+ overflow: hidden;
+ }
+ #loading-bar-fill {
+ height: 100%; width: 0%;
+ background: linear-gradient(90deg, #00aa22, #00ff41);
+ box-shadow: 0 0 10px #00ff41;
+ transition: width 0.3s ease-out;
+ }
+
+ /* Percent + status */
+ #loading-percent {
+ color: #00ff41; font-size: 11px; letter-spacing: 3px;
+ text-shadow: 0 0 8px #00ff41;
+ }
+ #loading-msg {
+ color: #007722; font-size: 10px; letter-spacing: 4px;
+ }
#ui-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
@@ -162,7 +205,14 @@
-
RECOVERING WebGL CONTEXT…
diff --git a/js/loading.js b/js/loading.js
new file mode 100644
index 0000000..7a707e6
--- /dev/null
+++ b/js/loading.js
@@ -0,0 +1,86 @@
+/**
+ * Loading screen — ASCII art Timmy logo with progressive character reveal.
+ *
+ * Usage:
+ * import { initLoadingArt, setLoadingProgress } from './loading.js';
+ * initLoadingArt(); // call once on page load
+ * setLoadingProgress(50, 'MSG'); // 0–100, optional status message
+ */
+
+const TIMMY_ASCII = `
+ ████████╗██╗███╗ ███╗███╗ ███╗██╗ ██╗
+ ╚══██╔══╝██║████╗ ████║████╗ ████║╚██╗ ██╔╝
+ ██║ ██║██╔████╔██║██╔████╔██║ ╚████╔╝
+ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║ ╚██╔╝
+ ██║ ██║██║ ╚═╝ ██║██║ ╚═╝ ██║ ██║
+ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
+
+ ┌───────────────────────────────┐
+ │ SOVEREIGN AI · SOUL ON BTC │
+ └───────────────────────────────┘
+`.trimStart();
+
+/** All non-whitespace char indices in the ASCII art, randomised for scatter reveal */
+let _charSpans = [];
+let _totalChars = 0;
+
+/**
+ * Initialise the loading screen: build character spans for the ASCII art.
+ * Call once before the first setLoadingProgress().
+ */
+export function initLoadingArt() {
+ const logoEl = document.getElementById('ascii-logo');
+ if (!logoEl) return;
+
+ // Build span-per-char markup — whitespace stays as plain text nodes for layout.
+ const chars = [...TIMMY_ASCII];
+ let html = '';
+ let idx = 0;
+ for (const ch of chars) {
+ if (ch === '\n') {
+ html += '\n';
+ } else if (ch === ' ') {
+ html += ' ';
+ } else {
+ html += `
${ch}`;
+ idx++;
+ }
+ }
+ _totalChars = idx;
+ logoEl.innerHTML = html;
+
+ // Cache span elements for fast access
+ _charSpans = Array.from(logoEl.querySelectorAll('.ac'));
+}
+
+/**
+ * Update loading progress.
+ * @param {number} percent 0–100
+ * @param {string} [msg] Optional status line text
+ */
+export function setLoadingProgress(percent, msg) {
+ const pct = Math.max(0, Math.min(100, percent));
+
+ // How many chars should be "lit" at this progress level
+ const litCount = Math.round((_totalChars * pct) / 100);
+
+ for (let i = 0; i < _charSpans.length; i++) {
+ if (i < litCount) {
+ _charSpans[i].classList.add('lit');
+ }
+ }
+
+ // Progress bar fill
+ const bar = document.getElementById('loading-bar-fill');
+ if (bar) bar.style.width = `${pct}%`;
+
+ // Percentage label
+ const pctEl = document.getElementById('loading-percent');
+ if (pctEl) pctEl.textContent = `${Math.round(pct)}%`;
+
+ // Status message
+ if (msg) {
+ const msgEl = document.getElementById('loading-msg');
+ if (msgEl) msgEl.textContent = msg;
+ }
+}
diff --git a/js/main.js b/js/main.js
index a9185e2..814bddb 100644
--- a/js/main.js
+++ b/js/main.js
@@ -8,6 +8,7 @@ import { initUI, updateUI } from './ui.js';
import { initInteraction, updateControls, disposeInteraction } from './interaction.js';
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
import { initVisitor } from './visitor.js';
+import { initLoadingArt, setLoadingProgress } from './loading.js';
let running = false;
let canvas = null;
@@ -22,26 +23,40 @@ let canvas = null;
* Agent state map captured just before teardown; reapplied after initAgents.
*/
function buildWorld(firstInit, stateSnapshot) {
+ if (firstInit) setLoadingProgress(10, 'RENDERING ENGINE...');
const { scene, camera, renderer } = initWorld(canvas);
canvas = renderer.domElement;
+ if (firstInit) setLoadingProgress(30, 'EFFECTS SYSTEM...');
initEffects(scene);
+
+ if (firstInit) setLoadingProgress(50, 'SPAWNING AGENTS...');
initAgents(scene);
if (stateSnapshot) {
applyAgentStates(stateSnapshot);
}
+ if (firstInit) setLoadingProgress(70, 'ARMING CONTROLS...');
initInteraction(camera, renderer);
if (firstInit) {
+ setLoadingProgress(80, 'LOADING INTERFACE...');
initUI();
+
+ setLoadingProgress(90, 'CONNECTING TO GRID...');
initWebSocket(scene);
+
+ setLoadingProgress(95, 'INITIALIZING VISITOR...');
initVisitor();
- // Dismiss loading screen
+ setLoadingProgress(100, 'WELCOME TO TOWER WORLD');
+
+ // Dismiss loading screen after a brief moment so 100% is visible
const loadingScreen = document.getElementById('loading-screen');
- if (loadingScreen) loadingScreen.classList.add('hidden');
+ if (loadingScreen) {
+ setTimeout(() => loadingScreen.classList.add('hidden'), 400);
+ }
}
// Debounce resize to 1 call per frame
@@ -117,6 +132,9 @@ function teardown({ scene, renderer, ac }) {
function main() {
const $overlay = document.getElementById('webgl-recovery-overlay');
+ initLoadingArt();
+ setLoadingProgress(0, 'BOOTING MATRIX...');
+
let handle = buildWorld(true, null);
// WebGL context loss recovery (iPad PWA, GPU driver reset, etc.)