diff --git a/wolf3d-editor/app/src/components/SidePanel.jsx b/wolf3d-editor/app/src/components/SidePanel.jsx
index 3feb809..e35d949 100644
--- a/wolf3d-editor/app/src/components/SidePanel.jsx
+++ b/wolf3d-editor/app/src/components/SidePanel.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import { levelLabel, LIMITS } from '@wolf3d/data';
-import { store, currentLevel, updateUi } from '../store.js';
+import { store, currentLevel, updateUi, createLevel, deleteLevel } from '../store.js';
import { useStoreVersion } from '../hooks.js';
import { levelStats } from '../game/stats.js';
import { floorCodeColor } from '../game/assets.js';
@@ -39,8 +39,18 @@ export function SidePanel() {
minHeight: 0,
}}
>
-
+
LEVELS ({game.levels.filter(Boolean).length} in {game.ext.toUpperCase()})
+
{game.levels.map((l, i) =>
@@ -60,10 +70,24 @@ export function SidePanel() {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
+ display: 'flex',
+ alignItems: 'center',
}}
>
{levelLabel(i)}
- {l.name || '(unnamed)'}
+ {l.name || '(unnamed)'}
+ {store.ui.level === i && (
+ {
+ e.stopPropagation();
+ if (window.confirm(`Delete ${levelLabel(i)} "${l.name}"?`)) deleteLevel(i);
+ }}
+ >
+ ✕
+
+ )}
) : null,
)}
diff --git a/wolf3d-editor/app/src/store.js b/wolf3d-editor/app/src/store.js
index 4f45f3d..723b5d6 100644
--- a/wolf3d-editor/app/src/store.js
+++ b/wolf3d-editor/app/src/store.js
@@ -247,6 +247,47 @@ export function setSpritePixels(num, pixels) {
notify();
}
+/**
+ * Create a fresh level in an empty slot: solid grey-stone border, floor code
+ * 6C inside, player start in the middle (the engine requires exactly one).
+ * @param {number} slot
+ */
+export function createLevel(slot) {
+ const game = store.game;
+ if (!game || game.levels[slot]) return;
+ const plane0 = new Uint16Array(MAP_WIDTH * MAP_WIDTH);
+ const plane1 = new Uint16Array(MAP_WIDTH * MAP_WIDTH);
+ for (let y = 0; y < MAP_WIDTH; y++) {
+ for (let x = 0; x < MAP_WIDTH; x++) {
+ const border = x === 0 || y === 0 || x === MAP_WIDTH - 1 || y === MAP_WIDTH - 1;
+ plane0[y * MAP_WIDTH + x] = border ? 1 : 0x6c;
+ }
+ }
+ plane1[32 * MAP_WIDTH + 32] = 19; // player start facing north
+ game.levels[slot] = { name: `New Map ${slot + 1}`, plane0, plane1, plane2: null };
+ store.ui.level = slot;
+ store.ui.dirty = true;
+ notify();
+}
+
+/**
+ * Remove a level from its slot.
+ * @param {number} slot
+ */
+export function deleteLevel(slot) {
+ const game = store.game;
+ if (!game || !game.levels[slot]) return;
+ game.levels[slot] = null;
+ store.undoStack = store.undoStack.filter((r) => r.level !== slot);
+ store.redoStack = store.redoStack.filter((r) => r.level !== slot);
+ if (store.ui.level === slot) {
+ const next = game.levels.findIndex(Boolean);
+ store.ui.level = next >= 0 ? next : 0;
+ }
+ store.ui.dirty = true;
+ notify();
+}
+
/** Mutate UI state and notify. @param {(ui: typeof store.ui) => void} fn */
export function updateUi(fn) {
fn(store.ui);
diff --git a/wolf3d-editor/app/test/zip.test.js b/wolf3d-editor/app/test/zip.test.js
new file mode 100644
index 0000000..b05e6c0
--- /dev/null
+++ b/wolf3d-editor/app/test/zip.test.js
@@ -0,0 +1,24 @@
+import { describe, it, expect } from 'vitest';
+import { buildZip, readZip, crc32 } from '../src/io/zip.js';
+
+describe('zip container', () => {
+ it('round-trips entries', async () => {
+ const entries = [
+ { name: 'GAMEMAPS.WL6', data: Uint8Array.from({ length: 5000 }, (_, i) => (i * 7) & 0xff) },
+ { name: 'MAPHEAD.WL6', data: Uint8Array.from({ length: 402 }, (_, i) => i & 0xff) },
+ { name: 'dir/readme.txt', data: new TextEncoder().encode('hello wolf') },
+ ];
+ const zip = buildZip(entries);
+ const back = await readZip(zip);
+ expect(back).toHaveLength(3);
+ for (let i = 0; i < entries.length; i++) {
+ expect(back[i].name).toBe(entries[i].name);
+ expect(back[i].data).toEqual(entries[i].data);
+ }
+ });
+
+ it('computes standard CRC32', () => {
+ // CRC32 of "123456789" is 0xCBF43926 (the canonical check value).
+ expect(crc32(new TextEncoder().encode('123456789'))).toBe(0xcbf43926);
+ });
+});