This commit is contained in:
Renan LE CARO 2025-04-22 16:37:56 +02:00
parent b8daf018b1
commit 181e156f60
6 changed files with 1061 additions and 1132 deletions

View file

@ -17,7 +17,8 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
## Done ## Done
- categorize and color perks -
- color coded perks (green = noob friendly, red = combo with reset condition)
- removed : instant_upgrade - removed : instant_upgrade
- nerfed : helium : now need to be level 3 to have the same effect of keeping coins up - nerfed : helium : now need to be level 3 to have the same effect of keeping coins up

1574
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,297 +1,270 @@
import { icons, transformRawLevel } from "./loadGameData"; import {icons, transformRawLevel} from "./loadGameData";
import { t } from "./i18n/i18n"; import {t} from "./i18n/i18n";
import { getSettingValue, getTotalScore, setSettingValue } from "./settings"; import {getSettingValue, getTotalScore, setSettingValue} from "./settings";
import { asyncAlert } from "./asyncAlert"; import {asyncAlert} from "./asyncAlert";
import { Palette, RawLevel } from "./types"; import {Palette, RawLevel} from "./types";
import { levelIconHTML } from "./levelIcon"; import {levelIconHTML} from "./levelIcon";
import _palette from "./data/palette.json"; import _palette from "./data/palette.json";
import { restart } from "./game"; import {restart} from "./game";
import { describeLevel } from "./game_utils"; import {describeLevel} from "./game_utils";
import {automaticBackgroundColor, levelCodeToRawLevel, MAX_LEVEL_SIZE, MIN_LEVEL_SIZE} from "./pure_functions";
const palette = _palette as Palette; const palette = _palette as Palette;
const MAX_LEVEL_SIZE = 21;
const MIN_LEVEL_SIZE = 2;
export function levelEditorMenuEntry() { export function levelEditorMenuEntry() {
const min = 10000; const min = 10000;
const disabled = getTotalScore() < min; const disabled = getTotalScore() < min;
return { return {
icon: icons["icon:editor"], icon: icons["icon:editor"],
text: t("editor.title"), text: t("editor.title"),
disabled, disabled,
help: disabled ? t("editor.locked", { min }) : t("editor.help"), help: disabled ? t("editor.locked", {min}) : t("editor.help"),
async value() { async value() {
openLevelEditorLevelsList().then(); openLevelEditorLevelsList().then();
}, },
}; };
} }
async function openLevelEditorLevelsList() { async function openLevelEditorLevelsList() {
const rawList = getSettingValue("custom_levels", []) as RawLevel[]; const rawList = getSettingValue("custom_levels", []) as RawLevel[];
const customLevels = rawList.map(transformRawLevel); const customLevels = rawList.map(transformRawLevel);
let choice = await asyncAlert({ let choice = await asyncAlert({
title: t("editor.title"), title: t("editor.title"),
content: [ content: [
...customLevels.map((l, li) => ({ ...customLevels.map((l, li) => ({
text: l.name, text: l.name,
icon: levelIconHTML(l.bricks, l.size, l.color), icon: levelIconHTML(l.bricks, l.size, l.color),
value() { value() {
editRawLevelList(li); editRawLevelList(li);
}, },
help: l.credit || describeLevel(l), help: l.credit || describeLevel(l),
})), })),
{ {
text: t("editor.new_level"), text: t("editor.new_level"),
icon: icons["icon:editor"], icon: icons["icon:editor"],
value() { value() {
rawList.push({ rawList.push({
color: "", color: "",
size: 6, size: 6,
bricks: "____________________________________", bricks: "____________________________________",
name: "custom level" + (rawList.length + 1), name: "custom level" + (rawList.length + 1),
credit: "", credit: "",
}); });
setSettingValue("custom_levels", rawList); setSettingValue("custom_levels", rawList);
editRawLevelList(rawList.length - 1); editRawLevelList(rawList.length - 1);
}, },
}, },
{ {
text: t("editor.import"), text: t("editor.import"),
help: t("editor.import_instruction"), help: t("editor.import_instruction"),
value() { value() {
const code = prompt(t("editor.import_instruction"))?.trim(); const code = prompt(t("editor.import_instruction"))?.trim();
if (code) { if (code) {
let [name, credit] = code.match(/\[([^\]]+)]/gi); const lvl = levelCodeToRawLevel(code)
if (lvl) {
let bricks = code rawList.push(lvl);
.split(name)[1] setSettingValue("custom_levels", rawList);
.split(credit)[0] }
.replace(/\s/gi, ""); }
name = name.slice(1, -1); openLevelEditorLevelsList();
credit = credit.slice(1, -1); },
name ||= "Imported on " + new Date().toISOString().slice(0, 10); },
credit ||= ""; ],
const size = Math.sqrt(bricks.length); });
if ( if (typeof choice == "function") choice();
Math.floor(size) === size &&
size >= MIN_LEVEL_SIZE &&
size <= MAX_LEVEL_SIZE
) {
rawList.push({
color: automaticBackgroundColor(bricks.split("")),
size,
bricks,
name,
credit,
});
setSettingValue("custom_levels", rawList);
}
}
openLevelEditorLevelsList();
},
},
],
});
if (typeof choice == "function") choice();
} }
export async function editRawLevelList(nth: number, color = "W") { export async function editRawLevelList(nth: number, color = "W") {
let rawList = getSettingValue("custom_levels", []) as RawLevel[]; let rawList = getSettingValue("custom_levels", []) as RawLevel[];
const level = rawList[nth]; const level = rawList[nth];
const bricks = level.bricks.split(""); const bricks = level.bricks.split("");
let grid = ""; let grid = "";
for (let y = 0; y < level.size; y++) { for (let y = 0; y < level.size; y++) {
grid += '<div style="background: ' + (level.color || "black") + ';">'; grid += '<div style="background: ' + (level.color || "black") + ';">';
for (let x = 0; x < level.size; x++) {
const c = bricks[y * level.size + x];
grid += `<span data-resolve-to="paint_brick:${x}:${y}" style="background: ${palette[c]}">${c == "B" ? "💣" : ""}</span>`;
}
grid += "</div>";
}
const levelColors = new Set(bricks);
levelColors.delete("_");
levelColors.delete("B");
let colorList =
'<div class="palette">' +
Object.entries(palette)
.filter(([key, value]) => key !== "_")
.filter(
([key, value]) =>
levelColors.size < 5 || levelColors.has(key) || key === "B",
)
.map(
([key, value]) =>
`<span data-resolve-to="set_color:${key}" data-selected="${key == color}" style="background: ${value}">${key == "B" ? "💣" : ""}</span>`,
)
.join("") +
"</div>";
const clicked = await asyncAlert<string | null>({
title: t("editor.editing.title", { name: level.name }),
content: [
t("editor.editing.color"),
colorList,
t("editor.editing.help"),
`<div class="gridEdit" style="--grid-size:${level.size};">${grid}</div>`,
{
icon: icons["icon:new_run"],
text: t("editor.editing.play"),
value: "play",
},
{
text: t("editor.editing.rename"),
value: "rename",
help: level.name,
},
{
text: t("editor.editing.credit"),
value: "credit",
help: level.credit,
},
{
text: t("editor.editing.delete"),
value: "delete",
},
{
text: t("editor.editing.copy"),
value: "copy",
help: t("editor.editing.copy_help"),
},
{
text: t("editor.editing.bigger"),
value: "size:+1",
disabled: level.size >= MAX_LEVEL_SIZE,
},
{
text: t("editor.editing.smaller"),
value: "size:-1",
disabled: level.size <= MIN_LEVEL_SIZE,
},
{
text: t("editor.editing.left"),
value: "move:-1:0",
},
{
text: t("editor.editing.right"),
value: "move:1:0",
},
{
text: t("editor.editing.up"),
value: "move:0:-1",
},
{
text: t("editor.editing.down"),
value: "move:0:1",
},
],
});
if (!clicked) return;
if (typeof clicked === "string") {
const [action, a, b] = clicked.split(":");
if (action == "paint_brick") {
const x = parseInt(a),
y = parseInt(b);
bricks[y * level.size + x] =
bricks[y * level.size + x] === color ? "_" : color;
level.bricks = bricks.join("");
}
if (action == "set_color") {
color = a;
}
if (action == "size") {
const newSize = level.size + parseInt(a);
const newBricks = [];
for (let y = 0; y < newSize; y++) {
for (let x = 0; x < newSize; x++) {
newBricks.push(
(x < level.size && y < level.size && bricks[y * level.size + x]) ||
"_",
);
}
}
level.size = newSize;
level.bricks = newBricks.join("");
}
if (action == "move") {
const dx = parseInt(a),
dy = parseInt(b);
const newBricks = [];
for (let y = 0; y < level.size; y++) {
for (let x = 0; x < level.size; x++) { for (let x = 0; x < level.size; x++) {
const tx = x - dx; const c = bricks[y * level.size + x];
const ty = y - dy; grid += `<span data-resolve-to="paint_brick:${x}:${y}" style="background: ${palette[c]}">${c == "B" ? "💣" : ""}</span>`;
if (tx < 0 || tx >= level.size || ty < 0 || ty >= level.size) {
newBricks.push("_");
} else {
newBricks.push(bricks[ty * level.size + tx]);
}
} }
} grid += "</div>";
level.bricks = newBricks.join("");
} }
if (action === "play") {
restart({
level: transformRawLevel(level),
isEditorTrialRun: nth,
perks: {
base_combo: 7,
},
});
return;
}
if (action === "copy") {
let text =
"```\n[" +
(level.name || "unnamed level")?.replace(/\[|\]/gi, " ") +
"]";
bricks.forEach((b, bi) => {
if (!(bi % level.size)) text += "\n";
text += b;
});
text +=
"\n[" +
(level.credit?.replace(/\[|\]/gi, " ") || "Missing credits") +
"]\n```";
navigator.clipboard.writeText(text);
// return
}
if (action === "rename") {
const name = prompt(t("editor.editing.rename_prompt"), level.name);
if (name) {
level.name = name;
}
}
if (action === "credit") {
const credit = prompt(
t("editor.editing.credit_prompt"),
level.credit || "",
);
if (credit !== "null") {
level.credit = credit || "";
}
}
if (action === "delete") {
rawList = rawList.filter((l, li) => li !== nth);
setSettingValue("custom_levels", rawList);
openLevelEditorLevelsList();
return;
}
}
level.color = automaticBackgroundColor(bricks); const levelColors = new Set(bricks);
levelColors.delete("_");
levelColors.delete("B");
setSettingValue("custom_levels", rawList); let colorList =
editRawLevelList(nth, color); '<div class="palette">' +
Object.entries(palette)
.filter(([key, value]) => key !== "_")
.filter(
([key, value]) =>
levelColors.size < 5 || levelColors.has(key) || key === "B",
)
.map(
([key, value]) =>
`<span data-resolve-to="set_color:${key}" data-selected="${key == color}" style="background: ${value}">${key == "B" ? "💣" : ""}</span>`,
)
.join("") +
"</div>";
const clicked = await asyncAlert<string | null>({
title: t("editor.editing.title", {name: level.name}),
content: [
t("editor.editing.color"),
colorList,
t("editor.editing.help"),
`<div class="gridEdit" style="--grid-size:${level.size};">${grid}</div>`,
{
icon: icons["icon:new_run"],
text: t("editor.editing.play"),
value: "play",
},
{
text: t("editor.editing.rename"),
value: "rename",
help: level.name,
},
{
text: t("editor.editing.credit"),
value: "credit",
help: level.credit,
},
{
text: t("editor.editing.delete"),
value: "delete",
},
{
text: t("editor.editing.copy"),
value: "copy",
help: t("editor.editing.copy_help"),
},
{
text: t("editor.editing.bigger"),
value: "size:+1",
disabled: level.size >= MAX_LEVEL_SIZE,
},
{
text: t("editor.editing.smaller"),
value: "size:-1",
disabled: level.size <= MIN_LEVEL_SIZE,
},
{
text: t("editor.editing.left"),
value: "move:-1:0",
},
{
text: t("editor.editing.right"),
value: "move:1:0",
},
{
text: t("editor.editing.up"),
value: "move:0:-1",
},
{
text: t("editor.editing.down"),
value: "move:0:1",
},
],
});
if (!clicked) return;
if (typeof clicked === "string") {
const [action, a, b] = clicked.split(":");
if (action == "paint_brick") {
const x = parseInt(a),
y = parseInt(b);
bricks[y * level.size + x] =
bricks[y * level.size + x] === color ? "_" : color;
level.bricks = bricks.join("");
}
if (action == "set_color") {
color = a;
}
if (action == "size") {
const newSize = level.size + parseInt(a);
const newBricks = [];
for (let y = 0; y < newSize; y++) {
for (let x = 0; x < newSize; x++) {
newBricks.push(
(x < level.size && y < level.size && bricks[y * level.size + x]) ||
"_",
);
}
}
level.size = newSize;
level.bricks = newBricks.join("");
}
if (action == "move") {
const dx = parseInt(a),
dy = parseInt(b);
const newBricks = [];
for (let y = 0; y < level.size; y++) {
for (let x = 0; x < level.size; x++) {
const tx = x - dx;
const ty = y - dy;
if (tx < 0 || tx >= level.size || ty < 0 || ty >= level.size) {
newBricks.push("_");
} else {
newBricks.push(bricks[ty * level.size + tx]);
}
}
}
level.bricks = newBricks.join("");
}
if (action === "play") {
restart({
level: transformRawLevel(level),
isEditorTrialRun: nth,
perks: {
base_combo: 7,
},
});
return;
}
if (action === "copy") {
let text =
"```\n[" +
(level.name || "unnamed level")?.replace(/\[|\]/gi, " ") +
"]";
bricks.forEach((b, bi) => {
if (!(bi % level.size)) text += "\n";
text += b;
});
text +=
"\n[" +
(level.credit?.replace(/\[|\]/gi, " ") || "Missing credits") +
"]\n```";
navigator.clipboard.writeText(text);
// return
}
if (action === "rename") {
const name = prompt(t("editor.editing.rename_prompt"), level.name);
if (name) {
level.name = name;
}
}
if (action === "credit") {
const credit = prompt(
t("editor.editing.credit_prompt"),
level.credit || "",
);
if (credit !== "null") {
level.credit = credit || "";
}
}
if (action === "delete") {
rawList = rawList.filter((l, li) => li !== nth);
setSettingValue("custom_levels", rawList);
openLevelEditorLevelsList();
return;
}
}
level.color = automaticBackgroundColor(bricks);
setSettingValue("custom_levels", rawList);
editRawLevelList(nth, color);
} }
export function automaticBackgroundColor(bricks: string[]) {
return bricks.filter((b) => b === "g").length >
bricks.filter((b) => b !== "_").length * 0.05
? "#115988"
: "";
}

View file

@ -6,6 +6,7 @@ import { getLevelBackground, hashCode } from "../getLevelBackground";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { moveLevel, resizeLevel, setBrick } from "./levels_editor_util"; import { moveLevel, resizeLevel, setBrick } from "./levels_editor_util";
import {levelCodeToRawLevel} from "../pure_functions";
const backgrounds = _backgrounds as string[]; const backgrounds = _backgrounds as string[];
@ -90,17 +91,11 @@ function App() {
height: 40, height: 40,
position: "absolute", position: "absolute",
}} }}
></button>, >{ (color=="black" && '💣')||' '}</button>,
); );
} }
} }
const background = color
? { backgroundImage: "none", backgroundColor: color }
: {
backgroundImage: `url("data:image/svg+xml;UTF8,${encodeURIComponent(getLevelBackground(level) as string)}")`,
backgroundColor: "transparent",
};
return ( return (
<div key={li}> <div key={li}>
@ -141,31 +136,12 @@ function App() {
<button onClick={() => updateLevel(li, moveLevel(level, 0, 1))}> <button onClick={() => updateLevel(li, moveLevel(level, 0, 1))}>
D D
</button> </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>
<div <div
className="level-bricks-preview" className="level-bricks-preview"
style={{ style={{
width: size * 40, width: size * 40,
height: size * 40, height: size * 40
...background,
}} }}
> >
{brickButtons} {brickButtons}
@ -180,14 +156,14 @@ function App() {
key={code} key={code}
className={code === selected ? "active" : ""} className={code === selected ? "active" : ""}
style={{ style={{
background: color || "linear-gradient(45deg,black,white)", background: color || "",
display: "inline-block", display: "inline-block",
width: "40px", width: "40px",
height: "40px", height: "40px",
border: "1px solid black", border: "1px solid black",
}} }}
onClick={() => setSelected(code)} onClick={() => setSelected(code)}
></button> >{(color=='' && 'x') || (color=="black" && '💣')||' '}</button>
))} ))}
</div> </div>
<button <button
@ -211,6 +187,21 @@ function App() {
> >
new new
</button> </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> </div>
); );
} }

View file

@ -5,7 +5,8 @@ import _appVersion from "./data/version.json";
import { rawUpgrades } from "./upgrades"; import { rawUpgrades } from "./upgrades";
import { getLevelBackground } from "./getLevelBackground"; import { getLevelBackground } from "./getLevelBackground";
import { levelIconHTML } from "./levelIcon"; import { levelIconHTML } from "./levelIcon";
import {automaticBackgroundColor} from "./levelEditor";
import {automaticBackgroundColor} from "./pure_functions";
const palette = _palette as Palette; const palette = _palette as Palette;

View file

@ -1,4 +1,4 @@
import { Ball, GameState } from "./types"; import {Ball, GameState} from "./types";
export function clamp(value: number, min: number, max: number) { export function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(value, max)); return Math.max(min, Math.min(value, max));
@ -102,3 +102,38 @@ export const wallBouncedBest = 3,
catchRateGood = 90, catchRateGood = 90,
missesBest = 3, missesBest = 3,
missesGood = 6; missesGood = 6;
export const MAX_LEVEL_SIZE = 21;
export const MIN_LEVEL_SIZE = 2;
export function automaticBackgroundColor(bricks: string[]) {
return bricks.filter((b) => b === "g").length >
bricks.filter((b) => b !== "_").length * 0.05
? "#115988"
: "";
}
export function levelCodeToRawLevel(code: string) {
let [name, credit] = code.match(/\[([^\]]+)]/gi);
let bricks = code
.split(name)[1]
.split(credit)[0]
.replace(/\s/gi, "");
name = name.slice(1, -1);
credit = credit.slice(1, -1);
name ||= "Imported on " + new Date().toISOString().slice(0, 10);
credit ||= "";
const size = Math.sqrt(bricks.length);
if (Math.floor(size) === size &&
size >= MIN_LEVEL_SIZE &&
size <= MAX_LEVEL_SIZE)
return {
color: automaticBackgroundColor(bricks.split("")),
size,
bricks,
name,
credit,
}
}