Files
major_tom/web/shell.html
Thomas 302bcc592d Rebind Space to jump, gate editor behind localStorage flag
Space is now the alternate jump key (was shoot). Web shell shows
game controls by default and hides editor UI unless
localStorage.show_editor is set to 'true'. The E key and ?edit URL
are blocked when the editor is not enabled.

Also fix Makefile to track web/shell.html as a WASM link dependency
so shell changes trigger a rebuild.
2026-03-01 18:24:21 +00:00

289 lines
8.9 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jump 'n Run</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: monospace;
color: #e0e0e0;
}
#canvas-container {
position: relative;
border: 2px solid #333;
background: #000;
}
canvas.emscripten {
display: block;
width: 1280px;
height: 720px;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
#status {
margin-top: 12px;
font-size: 14px;
color: #888;
}
#progress-bar {
width: 320px;
height: 6px;
background: #333;
border-radius: 3px;
margin-top: 8px;
overflow: hidden;
}
#progress-bar-inner {
height: 100%;
width: 0%;
background: #4ecdc4;
transition: width 0.2s;
}
.hidden { display: none !important; }
#controls {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
font-size: 12px;
flex-wrap: wrap;
justify-content: center;
max-width: 1300px;
}
#hint { color: #666; }
#game-controls {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
font-size: 12px;
color: #666;
flex-wrap: wrap;
justify-content: center;
max-width: 1300px;
}
.ctrl-btn {
color: #4ecdc4;
background: transparent;
border: 1px solid #4ecdc4;
padding: 2px 8px;
border-radius: 3px;
font-family: monospace;
font-size: 12px;
cursor: pointer;
text-decoration: none;
}
.ctrl-btn:hover {
background: #4ecdc4;
color: #1a1a2e;
}
#level-select {
background: #1a1a2e;
color: #4ecdc4;
border: 1px solid #4ecdc4;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
font-size: 12px;
cursor: pointer;
}
#level-select option {
background: #1a1a2e;
color: #e0e0e0;
}
.ctrl-sep {
color: #333;
user-select: none;
}
</style>
</head>
<body>
<div id="canvas-container">
<canvas class="emscripten" id="canvas" tabindex="1"
width="640" height="360"></canvas>
</div>
<div id="controls" class="hidden">
<a class="ctrl-btn" id="editor-link" href="?edit">Editor</a>
<span class="ctrl-sep">|</span>
<button class="ctrl-btn" id="btn-save" title="Save level (download .lvl)">Save</button>
<button class="ctrl-btn" id="btn-load" title="Load .lvl from disk">Load</button>
<span class="ctrl-sep">|</span>
<select id="level-select" title="Open a built-in level in the editor">
<option value="">-- Open level --</option>
</select>
<span class="ctrl-sep">|</span>
<span id="hint">E=editor P=test play 1-6=tools</span>
</div>
<div id="game-controls">
<span>Arrows=move</span>
<span class="ctrl-sep">|</span>
<span>Z/Space=jump</span>
<span class="ctrl-sep">|</span>
<span>X=shoot</span>
<span class="ctrl-sep">|</span>
<span>C=dash</span>
<span class="ctrl-sep">|</span>
<span>Up=aim up</span>
</div>
<div id="status">Loading...</div>
<div id="progress-bar"><div id="progress-bar-inner"></div></div>
<script>
/* ── Editor gate: only enable when localStorage show_editor=true ── */
var editorEnabled = false;
try { editorEnabled = (localStorage.getItem('show_editor') === 'true'); } catch(e) {}
if (editorEnabled) {
document.getElementById('controls').classList.remove('hidden');
document.getElementById('game-controls').classList.add('hidden');
}
/* Strip ?edit from URL when editor is not enabled */
if (!editorEnabled && window.location.search.indexOf('edit') !== -1) {
window.history.replaceState({}, '', window.location.pathname);
}
var statusEl = document.getElementById('status');
var progressEl = document.getElementById('progress-bar');
var progressIn = document.getElementById('progress-bar-inner');
var Module = {
canvas: document.getElementById('canvas'),
/* Force 1:1 pixel mapping so SDL_RenderSetLogicalSize doesn't
compute a viewport offset on HiDPI / fractional-scale displays */
devicePixelRatio: 1,
print: function(text) { console.log(text); },
printErr: function(text) { console.error(text); },
setStatus: function(text) {
if (!text) {
statusEl.classList.add('hidden');
progressEl.classList.add('hidden');
return;
}
/* Parse "Downloading data... (X/Y)" progress messages */
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
if (m) {
var pct = (parseInt(m[2]) / parseInt(m[4])) * 100;
progressIn.style.width = pct + '%';
}
statusEl.textContent = text;
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
Module.setStatus(left
? 'Loading... (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')'
: '');
}
};
Module.setStatus('Loading...');
window.onerror = function() {
Module.setStatus('Error - check the browser console');
};
/* ── Keyboard shortcuts: Ctrl+S / Ctrl+O / E-key ── */
document.addEventListener('keydown', function(e) {
/* Block E-key editor entry when editor is not enabled */
if (!editorEnabled && e.key === 'e' && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
e.stopPropagation();
return;
}
if (editorEnabled && (e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
e.stopPropagation();
if (typeof _editor_save_flag_ptr === 'function') {
HEAP32[_editor_save_flag_ptr() >> 2] = 1;
}
}
if (editorEnabled && (e.ctrlKey || e.metaKey) && e.key === 'o') {
e.preventDefault();
e.stopPropagation();
if (typeof _editor_load_flag_ptr === 'function') {
HEAP32[_editor_load_flag_ptr() >> 2] = 1;
}
}
}, true); /* useCapture=true to intercept before SDL */
/* ── Editor-only features (button handlers, level picker) ── */
if (editorEnabled) {
document.getElementById('btn-save').addEventListener('click', function() {
if (typeof _editor_save_flag_ptr === 'function') {
HEAP32[_editor_save_flag_ptr() >> 2] = 1;
}
/* Return focus to canvas so keys keep working */
document.getElementById('canvas').focus();
});
document.getElementById('btn-load').addEventListener('click', function() {
if (typeof _editor_load_flag_ptr === 'function') {
HEAP32[_editor_load_flag_ptr() >> 2] = 1;
}
document.getElementById('canvas').focus();
});
/* ── Level picker dropdown ──────────────────────── */
var levelSelect = document.getElementById('level-select');
/* Populate the dropdown after the module is ready (FS available) */
Module.postRun = Module.postRun || [];
Module.postRun.push(function() {
/* Scan for .lvl files in the virtual FS */
var levels = [];
try {
var entries = FS.readdir('assets/levels');
for (var i = 0; i < entries.length; i++) {
if (entries[i].endsWith('.lvl') && entries[i][0] !== '_') {
levels.push('assets/levels/' + entries[i]);
}
}
levels.sort();
} catch(e) {
console.error('Could not read levels dir:', e);
}
for (var j = 0; j < levels.length; j++) {
var opt = document.createElement('option');
opt.value = levels[j];
/* Show just the filename without path */
opt.textContent = levels[j].replace('assets/levels/', '');
levelSelect.appendChild(opt);
}
});
levelSelect.addEventListener('change', function() {
var path = this.value;
if (!path) return;
if (typeof _editor_load_vfs_file === 'function') {
/* Pass the path string to C */
var len = lengthBytesUTF8(path) + 1;
var buf = _malloc(len);
stringToUTF8(path, buf, len);
_editor_load_vfs_file(buf);
_free(buf);
}
/* Reset dropdown to placeholder */
this.selectedIndex = 0;
document.getElementById('canvas').focus();
});
/* Update title if in editor mode */
if (window.location.search.indexOf('edit') !== -1) {
document.title = 'Jump \'n Run - Level Editor';
}
}
</script>
{{{ SCRIPT }}}
</body>
</html>