Compare commits
1 Commits
main
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4bbe157c2 |
@@ -19,8 +19,57 @@
|
|||||||
color: #6a6050;
|
color: #6a6050;
|
||||||
}
|
}
|
||||||
.placeholder h1 { font-size: 1.2rem; font-weight: normal; margin-bottom: 1rem; }
|
.placeholder h1 { font-size: 1.2rem; font-weight: normal; margin-bottom: 1rem; }
|
||||||
.placeholder p { font-size: 0.85rem; }
|
.placeholder p { font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||||
.placeholder a { color: #8a7f6a; }
|
.placeholder a { color: #8a7f6a; }
|
||||||
|
|
||||||
|
/* Connection status HUD */
|
||||||
|
#status-hud {
|
||||||
|
position: fixed;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: rgba(10, 10, 15, 0.85);
|
||||||
|
border: 1px solid #2a2520;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6a6050;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
#status-hud .status-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
#status-hud .status-line:last-child { margin-bottom: 0; }
|
||||||
|
#status-hud .dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#status-hud .dot.connecting { background: #b8860b; animation: pulse 1.2s ease-in-out infinite; }
|
||||||
|
#status-hud .dot.online { background: #4a9; }
|
||||||
|
#status-hud .dot.offline { background: #a44; }
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.3; }
|
||||||
|
}
|
||||||
|
#retry-btn {
|
||||||
|
display: none;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
background: #2a2520;
|
||||||
|
border: 1px solid #4a4030;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #8a7f6a;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#retry-btn:hover { background: #3a3530; color: #c0b8a8; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -31,6 +80,16 @@
|
|||||||
<p><a href="/">← Back to the Tower</a></p>
|
<p><a href="/">← Back to the Tower</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="status-hud">
|
||||||
|
<div class="status-line">
|
||||||
|
<span class="dot connecting" id="status-dot"></span>
|
||||||
|
<span id="status-text">INITIALIZING</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-line">AGENTS: <span id="agent-count">0</span></div>
|
||||||
|
<button id="retry-btn">Retry connection</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Reject unknown sub-paths: only /world/ is valid -->
|
<!-- Reject unknown sub-paths: only /world/ is valid -->
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
@@ -41,6 +100,6 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<!-- Three.js scene will mount to #scene -->
|
<!-- Three.js scene will mount to #scene -->
|
||||||
<!-- <script type="module" src="main.js"></script> -->
|
<script type="module" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
124
world/main.js
124
world/main.js
@@ -1,20 +1,124 @@
|
|||||||
/**
|
/**
|
||||||
* The Workshop — Three.js scene bootstrap
|
* The Workshop — Three.js scene bootstrap
|
||||||
*
|
*
|
||||||
* This file will initialize the 3D world where Timmy lives.
|
* Initializes the 3D world where Timmy lives.
|
||||||
* Currently a placeholder until tech decisions are made:
|
* Handles WebSocket connection to tower-hermes backend with
|
||||||
* - 3D engine confirmed (Three.js vs Babylon.js)
|
* timeout, retry, and clear status display.
|
||||||
* - Character design direction chosen
|
|
||||||
* - WebSocket bridge to Timmy's soul designed (#243)
|
|
||||||
*
|
*
|
||||||
* See: #242 (3D world), #243 (WebSocket bridge), #265 (presence schema)
|
* See: #242 (3D world), #243 (WebSocket bridge), #265 (presence schema)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Future: import * as THREE from 'three';
|
// Future: import * as THREE from 'three';
|
||||||
|
|
||||||
export function initWorkshop(container) {
|
const HERMES_WS_URL = (location.protocol === 'https:' ? 'wss://' : 'ws://') +
|
||||||
// TODO: Initialize 3D scene
|
location.host + '/ws/tower';
|
||||||
// TODO: Load wizard character model
|
const CONNECT_TIMEOUT_MS = 5000;
|
||||||
// TODO: Connect to Timmy presence WebSocket
|
const RETRY_DELAY_MS = 3000;
|
||||||
console.log('[Workshop] Scene container ready:', container.id);
|
const MAX_AUTO_RETRIES = 3;
|
||||||
|
|
||||||
|
const Status = { CONNECTING: 'connecting', ONLINE: 'online', OFFLINE: 'offline' };
|
||||||
|
|
||||||
|
const dom = {
|
||||||
|
dot: document.getElementById('status-dot'),
|
||||||
|
text: document.getElementById('status-text'),
|
||||||
|
agents: document.getElementById('agent-count'),
|
||||||
|
retryBtn: document.getElementById('retry-btn'),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ws = null;
|
||||||
|
let autoRetries = 0;
|
||||||
|
let connectTimer = null;
|
||||||
|
|
||||||
|
function setStatus(state, message) {
|
||||||
|
dom.dot.className = 'dot ' + state;
|
||||||
|
dom.text.textContent = message;
|
||||||
|
dom.retryBtn.style.display = state === Status.OFFLINE ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAgentCount(n) {
|
||||||
|
dom.agents.textContent = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
clearTimeout(connectTimer);
|
||||||
|
if (ws) {
|
||||||
|
ws.onopen = null;
|
||||||
|
ws.onclose = null;
|
||||||
|
ws.onerror = null;
|
||||||
|
ws.onmessage = null;
|
||||||
|
if (ws.readyState <= WebSocket.OPEN) ws.close();
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
cleanup();
|
||||||
|
setStatus(Status.CONNECTING, 'CONNECTING\u2026');
|
||||||
|
setAgentCount(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(HERMES_WS_URL);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Workshop] WebSocket creation failed:', err);
|
||||||
|
onFail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectTimer = setTimeout(function () {
|
||||||
|
console.warn('[Workshop] Connection timeout after ' + CONNECT_TIMEOUT_MS + 'ms');
|
||||||
|
cleanup();
|
||||||
|
onFail();
|
||||||
|
}, CONNECT_TIMEOUT_MS);
|
||||||
|
|
||||||
|
ws.onopen = function () {
|
||||||
|
clearTimeout(connectTimer);
|
||||||
|
autoRetries = 0;
|
||||||
|
setStatus(Status.ONLINE, 'ONLINE');
|
||||||
|
console.log('[Workshop] Connected to tower-hermes');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function (evt) {
|
||||||
|
try {
|
||||||
|
var msg = JSON.parse(evt.data);
|
||||||
|
if (typeof msg.agents === 'number') setAgentCount(msg.agents);
|
||||||
|
} catch (_) {
|
||||||
|
// non-JSON messages are ignored
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function () {
|
||||||
|
clearTimeout(connectTimer);
|
||||||
|
console.log('[Workshop] Connection closed');
|
||||||
|
onFail();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function () {
|
||||||
|
clearTimeout(connectTimer);
|
||||||
|
console.error('[Workshop] WebSocket error');
|
||||||
|
// onclose will fire after this, which calls onFail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFail() {
|
||||||
|
if (autoRetries < MAX_AUTO_RETRIES) {
|
||||||
|
autoRetries++;
|
||||||
|
setStatus(Status.CONNECTING, 'RETRYING (' + autoRetries + '/' + MAX_AUTO_RETRIES + ')\u2026');
|
||||||
|
setTimeout(connect, RETRY_DELAY_MS);
|
||||||
|
} else {
|
||||||
|
setStatus(Status.OFFLINE, 'OFFLINE \u2014 backend unreachable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual retry resets the counter
|
||||||
|
dom.retryBtn.addEventListener('click', function () {
|
||||||
|
autoRetries = 0;
|
||||||
|
connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Boot
|
||||||
|
export function initWorkshop(container) {
|
||||||
|
console.log('[Workshop] Scene container ready:', container.id);
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
initWorkshop(document.getElementById('scene'));
|
||||||
|
|||||||
Reference in New Issue
Block a user