breakout71/src/level_editor/levels_editor.tsx

225 lines
6.7 KiB
TypeScript
Raw Normal View History

2025-04-06 15:38:30 +02:00
import { Level, Palette, RawLevel } from "../types";
2025-03-16 17:45:29 +01:00
import _backgrounds from "../data/backgrounds.json";
import _palette from "../data/palette.json";
import { createRoot } from "react-dom/client";
2025-04-23 15:37:07 +02:00
import { useEffect, useState } from "react";
import { moveLevel, resizeLevel, setBrick } from "./levels_editor_util";
2025-04-23 10:56:50 +02:00
import {
automaticBackgroundColor,
levelCodeToRawLevel,
} from "../pure_functions";
2025-03-13 16:43:00 +01:00
const palette = _palette as Palette;
2025-04-06 15:38:30 +02:00
let allLevels = null;
2025-03-13 16:43:00 +01:00
function App() {
const [selected, setSelected] = useState("W");
const [applying, setApplying] = useState("");
2025-04-06 11:27:26 +02:00
const [levels, setLevels] = useState([]);
2025-04-06 15:38:30 +02:00
useEffect(() => {
fetch("http://localhost:4400/src/data/levels.json")
.then((r) => r.json())
2025-04-23 11:16:52 +02:00
.then((lvls) => {
2025-04-23 15:37:07 +02:00
const cleaned = lvls.map((l) => ({
name: l.name,
size: l.size,
bricks: (l.bricks + "_".repeat(l.size * l.size)).slice(
0,
l.size * l.size,
),
credit: l.credit || "",
}));
const sorted = [
...cleaned
.filter((l) => l.name.match("icon:"))
.sort((a, b) => (a.name > b.name ? 1 : -1)),
...cleaned.filter((l) => !l.name.match("icon:")),
];
setLevels(sorted as RawLevel[]);
2025-04-23 11:16:52 +02:00
allLevels = sorted;
2025-04-06 15:38:30 +02:00
});
}, []);
2025-04-06 11:27:26 +02:00
2025-04-06 15:38:30 +02:00
const updateLevel = (index: number, change: Partial<RawLevel>) => {
setLevels((list) =>
list.map((l, li) => (li === index ? { ...l, ...change } : l)),
);
};
2025-04-06 11:27:26 +02:00
const deleteLevel = (index: number) => {
if (confirm("Delete level")) {
2025-04-06 15:38:30 +02:00
setLevels(levels.filter((l, i) => i !== index));
}
2025-04-06 15:38:30 +02:00
};
useEffect(() => {
2025-04-06 15:38:30 +02:00
if (!allLevels || JSON.stringify(allLevels) === JSON.stringify(levels))
return;
const timoutId = setTimeout(() => {
2025-03-16 17:45:29 +01:00
return fetch("http://localhost:4400/src/data/levels.json", {
method: "POST",
headers: {
"Content-Type": "text/plain",
},
body: JSON.stringify(levels, null, 2),
});
}, 500);
return () => clearTimeout(timoutId);
}, [levels]);
return (
<div
onMouseUp={() => {
setApplying("");
}}
>
<div id={"levels"}>
{levels.map((level, li) => {
2025-03-31 20:13:47 +02:00
const { name, credit, bricks, size, svg, color } = level;
const brickButtons = [];
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
const index = y * size + x;
brickButtons.push(
<button
key={index}
onMouseDown={() => {
if (!applying) {
const color = selected === bricks[index] ? "_" : selected;
setApplying(color);
updateLevel(li, setBrick(level, index, color));
}
}}
onMouseEnter={() => {
if (applying) {
updateLevel(li, setBrick(level, index, applying));
2025-03-13 16:43:00 +01:00
}
}}
style={{
background: palette[bricks[index]] || "transparent",
left: x * 40,
top: y * 40,
width: 40,
height: 40,
position: "absolute",
}}
2025-04-23 10:56:50 +02:00
>
{(palette[bricks[index]] == "black" && "💣") || " "}
</button>,
);
2025-03-13 16:43:00 +01:00
}
}
return (
<div key={li}>
<input
2025-03-31 20:13:47 +02:00
className={"name"}
type="text"
value={name}
onChange={(e) => updateLevel(li, { name: e.target.value })}
/>
2025-03-31 20:08:17 +02:00
<input
2025-03-31 20:13:47 +02:00
className={"credit"}
2025-03-31 20:08:17 +02:00
type="text"
2025-03-31 20:13:47 +02:00
value={credit || ""}
2025-03-31 20:08:17 +02:00
onChange={(e) => updateLevel(li, { credit: e.target.value })}
/>
2025-03-31 20:13:47 +02:00
<div className={"buttons"}>
2025-04-06 11:27:26 +02:00
<button onClick={() => deleteLevel(li)}>Delete</button>
<button onClick={() => updateLevel(li, resizeLevel(level, -1))}>
-
</button>
<button onClick={() => updateLevel(li, resizeLevel(level, +1))}>
+
</button>
<button
onClick={() => updateLevel(li, moveLevel(level, -1, 0))}
>
L
</button>
<button onClick={() => updateLevel(li, moveLevel(level, 1, 0))}>
R
</button>
<button
onClick={() => updateLevel(li, moveLevel(level, 0, -1))}
>
U
</button>
<button onClick={() => updateLevel(li, moveLevel(level, 0, 1))}>
D
</button>
</div>
<div
className="level-bricks-preview"
style={{
width: size * 40,
2025-04-23 10:56:50 +02:00
height: size * 40,
background: automaticBackgroundColor(bricks.split("")),
}}
>
{brickButtons}
</div>
</div>
);
})}
</div>
<div id={"palette"}>
{Object.entries(palette).map(([code, color]) => (
<button
key={code}
className={code === selected ? "active" : ""}
style={{
2025-04-22 16:37:56 +02:00
background: color || "",
display: "inline-block",
width: "40px",
height: "40px",
border: "1px solid black",
}}
onClick={() => setSelected(code)}
2025-04-23 10:56:50 +02:00
>
{(color == "" && "x") || (color == "black" && "💣") || " "}
</button>
))}
</div>
<button
id="new-level"
onClick={() => {
const name = prompt("Name ? ");
if (!name) return;
setLevels((l) => [
...l,
2025-03-13 16:43:00 +01:00
{
name,
size: 8,
bricks:
"________________________________________________________________",
svg: null,
color: "",
},
]);
}}
>
new
</button>
2025-04-22 16:37:56 +02:00
<button
id="import-level"
onClick={() => {
const code = prompt("Level Code ? ");
2025-04-23 10:56:50 +02:00
if (!code) return;
const l = levelCodeToRawLevel(code);
if (!l) return;
setLevels((list) => [...list, l]);
2025-04-22 16:37:56 +02:00
}}
>
import
</button>
</div>
);
2025-03-13 16:43:00 +01:00
}
const root = createRoot(document.getElementById("app") as HTMLDivElement);
root.render(<App />);