Files
major_tom/web/shell.html
Thomas fac7085056 Add moon surface intro level with asteroid hazards and unarmed mechanics
Introduce moon01.lvl as the starting level — a pure jump-and-run intro
with no gun and no enemies, just platforming over gaps and dodging falling
asteroids. The player picks up their gun upon transitioning to level01.

New features:
- Moon tileset and PARALLAX_STYLE_MOON with crater terrain backgrounds
- Asteroid entity (ENT_ASTEROID): falls from sky, damages on contact,
  explodes on ground with particles, respawns after delay
- PLAYER_UNARMED directive disables gun for the level
- Pit rescue mechanic: falling costs 1 HP and auto-dashes upward
- Gun powerup entity type for future armed-pickup levels
- Segment-based procedural level generator with themed rooms
- Extended editor with entity palette and improved tile cycling
- Web shell improvements for Emscripten builds
2026-03-01 09:20:49 +00:00

247 lines
7.4 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; }
.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">
<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="status">Loading...</div>
<div id="progress-bar"><div id="progress-bar-inner"></div></div>
<script>
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 ────────── */
document.addEventListener('keydown', function(e) {
if ((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 ((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 */
/* ── Button handlers ────────────────────────────── */
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>