mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-26 15:06:16 -04:00
216 lines
6.5 KiB
TypeScript
216 lines
6.5 KiB
TypeScript
import { Level, Palette, RawLevel } from "../types";
|
|
import _backgrounds from "../data/backgrounds.json";
|
|
import _palette from "../data/palette.json";
|
|
import { createRoot } from "react-dom/client";
|
|
import { useEffect, useState } from "react";
|
|
import { moveLevel, resizeLevel, setBrick } from "./levels_editor_util";
|
|
import {
|
|
automaticBackgroundColor,
|
|
levelCodeToRawLevel,
|
|
} from "../pure_functions";
|
|
|
|
|
|
const palette = _palette as Palette;
|
|
|
|
let allLevels = null;
|
|
|
|
function App() {
|
|
const [selected, setSelected] = useState("W");
|
|
const [applying, setApplying] = useState("");
|
|
const [levels, setLevels] = useState([]);
|
|
useEffect(() => {
|
|
fetch("http://localhost:4400/src/data/levels.json")
|
|
.then((r) => r.json())
|
|
.then((lvls) => {
|
|
|
|
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[])
|
|
allLevels = sorted;
|
|
});
|
|
}, []);
|
|
|
|
const updateLevel = (index: number, change: Partial<RawLevel>) => {
|
|
setLevels((list) =>
|
|
list.map((l, li) => (li === index ? { ...l, ...change } : l)),
|
|
);
|
|
};
|
|
|
|
const deleteLevel = (index: number) => {
|
|
if (confirm("Delete level")) {
|
|
setLevels(levels.filter((l, i) => i !== index));
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
if (!allLevels || JSON.stringify(allLevels) === JSON.stringify(levels))
|
|
return;
|
|
const timoutId = setTimeout(() => {
|
|
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) => {
|
|
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));
|
|
}
|
|
}}
|
|
style={{
|
|
background: palette[bricks[index]] || "transparent",
|
|
left: x * 40,
|
|
top: y * 40,
|
|
width: 40,
|
|
height: 40,
|
|
position: "absolute",
|
|
}}
|
|
>
|
|
{(palette[bricks[index]] == "black" && "💣") || " "}
|
|
</button>,
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div key={li}>
|
|
<input
|
|
className={"name"}
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => updateLevel(li, { name: e.target.value })}
|
|
/>
|
|
<input
|
|
className={"credit"}
|
|
type="text"
|
|
value={credit || ""}
|
|
onChange={(e) => updateLevel(li, { credit: e.target.value })}
|
|
/>
|
|
|
|
<div className={"buttons"}>
|
|
<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,
|
|
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={{
|
|
background: color || "",
|
|
display: "inline-block",
|
|
width: "40px",
|
|
height: "40px",
|
|
border: "1px solid black",
|
|
}}
|
|
onClick={() => setSelected(code)}
|
|
>
|
|
{(color == "" && "x") || (color == "black" && "💣") || " "}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
id="new-level"
|
|
onClick={() => {
|
|
const name = prompt("Name ? ");
|
|
if (!name) return;
|
|
|
|
setLevels((l) => [
|
|
...l,
|
|
{
|
|
name,
|
|
size: 8,
|
|
bricks:
|
|
"________________________________________________________________",
|
|
svg: null,
|
|
color: "",
|
|
},
|
|
]);
|
|
}}
|
|
>
|
|
new
|
|
</button>
|
|
<button
|
|
id="import-level"
|
|
onClick={() => {
|
|
const code = prompt("Level Code ? ");
|
|
if (!code) return;
|
|
const l = levelCodeToRawLevel(code);
|
|
if (!l) return;
|
|
setLevels((list) => [...list, l]);
|
|
}}
|
|
>
|
|
import
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const root = createRoot(document.getElementById("app") as HTMLDivElement);
|
|
root.render(<App />);
|