mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-22 21:16:14 -04:00
Build 29035725
This commit is contained in:
parent
a1bf54af71
commit
819197031f
64 changed files with 3494 additions and 6921 deletions
47
src/level_editor/level_editor_util.test.ts
Normal file
47
src/level_editor/level_editor_util.test.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { moveLevel, resizeLevel, setBrick } from "./levels_editor_util";
|
||||
|
||||
const baseLevel = {
|
||||
name: "",
|
||||
bricks: "AAAA",
|
||||
size: 2,
|
||||
svg: null,
|
||||
color: "",
|
||||
};
|
||||
describe("resizeLevel", () => {
|
||||
it("should expand levels", () => {
|
||||
expect(resizeLevel(baseLevel, 1)).toStrictEqual({
|
||||
bricks: "AA_AA____",
|
||||
size: 3,
|
||||
});
|
||||
});
|
||||
it("should shrink levels", () => {
|
||||
expect(resizeLevel(baseLevel, -1)).toStrictEqual({ bricks: "A", size: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveLevel", () => {
|
||||
it("should do nothing when coords are 0/0", () => {
|
||||
expect(moveLevel(baseLevel, 0, 0)).toStrictEqual({ bricks: "AAAA" });
|
||||
});
|
||||
it("should move right", () => {
|
||||
expect(moveLevel(baseLevel, 1, 0)).toStrictEqual({ bricks: "_A_A" });
|
||||
});
|
||||
it("should move left", () => {
|
||||
expect(moveLevel(baseLevel, -1, 0)).toStrictEqual({ bricks: "A_A_" });
|
||||
});
|
||||
it("should move up", () => {
|
||||
expect(moveLevel(baseLevel, 0, -1)).toStrictEqual({ bricks: "AA__" });
|
||||
});
|
||||
it("should move down", () => {
|
||||
expect(moveLevel(baseLevel, 0, 1)).toStrictEqual({ bricks: "__AA" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("setBrick", () => {
|
||||
it("should set the first brick", () => {
|
||||
expect(setBrick(baseLevel, 0, "C")).toStrictEqual({ bricks: "CAAA" });
|
||||
});
|
||||
it("should any brick", () => {
|
||||
expect(setBrick(baseLevel, 2, "C")).toStrictEqual({ bricks: "AACA" });
|
||||
});
|
||||
});
|
53
src/level_editor/levels_editor.less
Normal file
53
src/level_editor/levels_editor.less
Normal file
|
@ -0,0 +1,53 @@
|
|||
body {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#palette {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 80px;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
|
||||
button.active {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
#levels {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
margin-right: 80px;
|
||||
|
||||
.level-bricks-preview {
|
||||
position: relative;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-areas: ". name" "buttons bricks";
|
||||
|
||||
& > *:nth-child(1) {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
& > div:nth-child(2) {
|
||||
grid-area: buttons;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
& > div:nth-child(3) {
|
||||
grid-area: bricks;
|
||||
}
|
||||
}
|
||||
}
|
203
src/level_editor/levels_editor.tsx
Normal file
203
src/level_editor/levels_editor.tsx
Normal file
|
@ -0,0 +1,203 @@
|
|||
import { Palette, RawLevel } from "../types";
|
||||
import _backgrounds from "../data/backgrounds.json";
|
||||
import _palette from "../data/palette.json";
|
||||
import _allLevels from "../data/levels.json";
|
||||
import { getLevelBackground, hashCode } from "../getLevelBackground";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { moveLevel, resizeLevel, setBrick } from "./levels_editor_util";
|
||||
|
||||
const backgrounds = _backgrounds as string[];
|
||||
|
||||
const palette = _palette as Palette;
|
||||
|
||||
let allLevels = _allLevels as RawLevel[];
|
||||
|
||||
function App() {
|
||||
const [selected, setSelected] = useState("W");
|
||||
const [applying, setApplying] = useState("");
|
||||
const [levels, setLevels] = useState(allLevels);
|
||||
const updateLevel = useCallback(
|
||||
(index: number, change: Partial<RawLevel>) => {
|
||||
setLevels((list) =>
|
||||
list.map((l, li) => (li === index ? { ...l, ...change } : l)),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const deleteLevel = useCallback((li: number) => {
|
||||
if (confirm("Delete level")) {
|
||||
setLevels(allLevels.filter((l, i) => i !== li));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
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, 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",
|
||||
}}
|
||||
></button>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const background = color
|
||||
? { backgroundImage: "none", backgroundColor: color }
|
||||
: {
|
||||
backgroundImage: `url("data:image/svg+xml;UTF8,${encodeURIComponent(getLevelBackground(level) as string)}")`,
|
||||
backgroundColor: "transparent",
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={li}>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => updateLevel(li, { name: e.target.value })}
|
||||
/>
|
||||
<div>
|
||||
<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>
|
||||
<input
|
||||
type="color"
|
||||
value={level.color || ""}
|
||||
onChange={(e) =>
|
||||
e.target.value && updateLevel(li, { color: e.target.value })
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={level.svg || hashCode(level.name) % backgrounds.length}
|
||||
onChange={(e) =>
|
||||
!isNaN(parseFloat(e.target.value)) &&
|
||||
updateLevel(li, {
|
||||
color: "",
|
||||
svg: parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="level-bricks-preview"
|
||||
style={{
|
||||
width: size * 40,
|
||||
height: size * 40,
|
||||
...background,
|
||||
}}
|
||||
>
|
||||
{brickButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div id={"palette"}>
|
||||
{Object.entries(palette).map(([code, color]) => (
|
||||
<button
|
||||
key={code}
|
||||
className={code === selected ? "active" : ""}
|
||||
style={{
|
||||
background: color || "linear-gradient(45deg,black,white)",
|
||||
display: "inline-block",
|
||||
width: "40px",
|
||||
height: "40px",
|
||||
border: "1px solid black",
|
||||
}}
|
||||
onClick={() => setSelected(code)}
|
||||
></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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById("app") as HTMLDivElement);
|
||||
root.render(<App />);
|
55
src/level_editor/levels_editor_util.ts
Normal file
55
src/level_editor/levels_editor_util.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { RawLevel } from "../types";
|
||||
|
||||
export function resizeLevel(level: RawLevel, sizeDelta: number) {
|
||||
const { size, bricks } = level;
|
||||
const newSize = Math.max(1, size + sizeDelta);
|
||||
const newBricks = [];
|
||||
for (let x = 0; x < newSize; x++) {
|
||||
for (let y = 0; y < newSize; y++) {
|
||||
newBricks[y * newSize + x] = brickAt(level, x, y);
|
||||
}
|
||||
}
|
||||
return {
|
||||
size: newSize,
|
||||
bricks: newBricks.join(""),
|
||||
};
|
||||
}
|
||||
|
||||
export function brickAt(level: RawLevel, x: number, y: number) {
|
||||
return (
|
||||
(x >= 0 &&
|
||||
x < level.size &&
|
||||
y >= 0 &&
|
||||
y < level.size &&
|
||||
level.bricks.split("")[y * level.size + x]) ||
|
||||
"_"
|
||||
);
|
||||
}
|
||||
|
||||
export function moveLevel(level: RawLevel, dx: number, dy: number) {
|
||||
const { size } = level;
|
||||
const newBricks = new Array(size * size).fill("_");
|
||||
for (let x = 0; x < size; x++) {
|
||||
for (let y = 0; y < size; y++) {
|
||||
newBricks[y * size + x] = brickAt(level, x - dx, y - dy);
|
||||
}
|
||||
}
|
||||
return {
|
||||
bricks: newBricks.join(""),
|
||||
};
|
||||
}
|
||||
|
||||
export function setBrick(level: RawLevel, index: number, colorCode: string) {
|
||||
const { size } = level;
|
||||
const newBricks = [];
|
||||
for (let x = 0; x < size; x++) {
|
||||
for (let y = 0; y < size; y++) {
|
||||
const brickIndex = y * size + x;
|
||||
newBricks[brickIndex] =
|
||||
(brickIndex === index && colorCode) || brickAt(level, x, y);
|
||||
}
|
||||
}
|
||||
return {
|
||||
bricks: newBricks.join(""),
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue