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
This commit is contained in:
Thomas
2026-03-01 09:20:49 +00:00
parent ea6e16358f
commit fac7085056
30 changed files with 2139 additions and 83 deletions

View File

@@ -23,6 +23,8 @@
}
canvas.emscripten {
display: block;
width: 1280px;
height: 720px;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
@@ -46,6 +48,51 @@
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>
@@ -53,6 +100,18 @@
<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>
@@ -63,6 +122,9 @@
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) {
@@ -92,6 +154,92 @@
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>