This commit is contained in:
Renan LE CARO 2025-04-14 13:39:30 +02:00
parent 11c797bc59
commit 64a85200b9
23 changed files with 1849 additions and 329 deletions

View file

@ -24,13 +24,14 @@ languages, I may add features again.
# Changelog # Changelog
## To do ## To do
- redo video
- allow loading newer save in outdated app (for rollback)
- level editor :english only, web only, link to test level, same link needs to be shared to discord
- auto-detect device performance at first startup and adjust settings accordingly - auto-detect device performance at first startup and adjust settings accordingly
- demo mode that shows device name (for phone shops to catch attention)
## Done ## Done
- level editor
- allow loading newer save in outdated app (for rollback)
- game crashes when reaching level 12 (no level info in runLevels) - game crashes when reaching level 12 (no level info in runLevels)
## 29074385 ## 29074385

View file

@ -29,8 +29,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29075517 versionCode = 29077162
versionName = "29075517" versionName = "29077162"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

File diff suppressed because one or more lines are too long

348
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
// The version of the cache. // The version of the cache.
const VERSION = "29075517"; const VERSION = "29077162";
// The name of the cache // The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`; const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -1,5 +1,5 @@
import { GameState, Level, PerkId, Upgrade } from "./types"; import { GameState, Level, PerkId, RawLevel, Upgrade } from "./types";
import { allLevels, icons, upgrades } from "./loadGameData"; import { allLevels, icons, transformRawLevel, upgrades } 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 { import {
@ -17,6 +17,7 @@ import {
} from "./game_utils"; } from "./game_utils";
import { getHistory } from "./gameOver"; import { getHistory } from "./gameOver";
import { noCreative } from "./upgrades"; import { noCreative } from "./upgrades";
import { levelIconHTML } from "./levelIcon";
export function creativeMode(gameState: GameState) { export function creativeMode(gameState: GameState) {
return { return {
@ -39,7 +40,9 @@ export async function openCreativeModePerksPicker() {
{}, {},
), ),
choice: Upgrade | Level | "reset" | void; choice: Upgrade | Level | "reset" | void;
const customLevels = (getSettingValue("custom_levels", []) as RawLevel[]).map(
transformRawLevel,
);
while ( while (
(choice = await asyncAlert<Upgrade | Level | "reset">({ (choice = await asyncAlert<Upgrade | Level | "reset">({
title: t("lab.menu_entry"), title: t("lab.menu_entry"),
@ -78,6 +81,13 @@ export async function openCreativeModePerksPicker() {
tooltip: problem || describeLevel(l), tooltip: problem || describeLevel(l),
}; };
}), }),
...customLevels.map((l) => ({
icon: levelIconHTML(l.bricks, l.size, l.color),
text: l.name,
value: l,
disabled: !l.bricks.filter((b) => b !== "_").length,
tooltip: describeLevel(l),
})),
], ],
})) }))
) { ) {
@ -88,7 +98,7 @@ export async function openCreativeModePerksPicker() {
} else if ("bricks" in choice) { } else if ("bricks" in choice) {
setSettingValue("creativeModePerks", creativeModePerks); setSettingValue("creativeModePerks", creativeModePerks);
if (await confirmRestart(gameState)) { if (await confirmRestart(gameState)) {
restart({ perks: creativeModePerks, level: choice.name }); restart({ perks: creativeModePerks, level: choice });
} }
return; return;
} else if (choice) { } else if (choice) {

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
"29075517" "29077162"

View file

@ -70,6 +70,7 @@ canvas:not(#game) {
color: gold; color: gold;
transition: color 0.01s; transition: color 0.01s;
} }
&.hidden { &.hidden {
display: none; display: none;
} }
@ -477,6 +478,7 @@ h2.histogram-title strong {
cursor: pointer; cursor: pointer;
background: black; background: black;
} }
td, td,
th { th {
padding: 0 5px; padding: 0 5px;
@ -488,11 +490,13 @@ h2.histogram-title strong {
td:first-child { td:first-child {
text-align: left; text-align: left;
} }
img { img {
width: 20px; width: 20px;
height: auto; height: auto;
pointer-events: none; pointer-events: none;
} }
tr:nth-child(2n) { tr:nth-child(2n) {
background: rgba(0, 0, 0, 0.58); background: rgba(0, 0, 0, 0.58);
} }
@ -508,6 +512,7 @@ h2.histogram-title strong {
height: 7px; height: 7px;
bottom: 2px; bottom: 2px;
border-radius: 2px; border-radius: 2px;
span { span {
position: absolute; position: absolute;
inset: 1px; inset: 1px;
@ -544,3 +549,40 @@ h2.histogram-title strong {
transform: none; transform: none;
} }
} }
.gridEdit > div > span,
.palette > span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: 1px solid;
cursor: pointer;
&:hover {
border-color: gold;
z-index: 1;
position: relative;
box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.2);
}
}
.gridEdit {
& > div {
display: flex;
& > span {
width: calc(min(500px, 100vw, 100vh - 200px) / var(--grid-size));
height: calc(min(500px, 100vw, 100vh - 200px) / var(--grid-size));
}
}
}
.palette {
display: flex;
flex-wrap: wrap;
& > span {
&[data-selected="true"] {
border: 2px solid white;
}
}
}

View file

@ -1,4 +1,10 @@
import { allLevels, appVersion, icons, upgrades } from "./loadGameData"; import {
allLevels,
allLevelsAndIcons,
appVersion,
icons,
upgrades,
} from "./loadGameData";
import { import {
Ball, Ball,
Coin, Coin,
@ -89,6 +95,7 @@ import { generateSaveFileContent } from "./generateSaveFileContent";
import { runHistoryViewerMenuEntry } from "./runHistoryViewer"; import { runHistoryViewerMenuEntry } from "./runHistoryViewer";
import { getNearestUnlockHTML, openScorePanel } from "./openScorePanel"; import { getNearestUnlockHTML, openScorePanel } from "./openScorePanel";
import { monitorLevelsUnlocks } from "./monitorLevelsUnlocks"; import { monitorLevelsUnlocks } from "./monitorLevelsUnlocks";
import { levelEditorMenuEntry } from "./levelEditor";
export async function play() { export async function play() {
if (await applyFullScreenChoice()) return; if (await applyFullScreenChoice()) return;
@ -518,6 +525,7 @@ export async function openMainMenu() {
}, },
creativeMode(gameState), creativeMode(gameState),
runHistoryViewerMenuEntry(), runHistoryViewerMenuEntry(),
levelEditorMenuEntry(),
{ {
icon: icons["icon:unlocks"], icon: icons["icon:unlocks"],
text: t("main_menu.unlocks"), text: t("main_menu.unlocks"),
@ -849,7 +857,10 @@ async function openUnlocksList() {
.map(({ name, id, threshold, icon, help }) => ({ .map(({ name, id, threshold, icon, help }) => ({
text: name, text: name,
disabled: ts < threshold, disabled: ts < threshold,
value: { perks: { [id]: 1 }, level: "icon:" + id } as RunParams, value: {
perks: { [id]: 1 },
level: allLevelsAndIcons.find((l) => l.name === "icon:" + id),
} as RunParams,
icon, icon,
[hintField]: [hintField]:
ts < threshold ts < threshold
@ -871,7 +882,7 @@ async function openUnlocksList() {
return { return {
text: l.name + percentUnlocked, text: l.name + percentUnlocked,
disabled: !!lockedBecause, disabled: !!lockedBecause,
value: { level: l.name } as RunParams, value: { level: l } as RunParams,
icon: icons[l.name], icon: icons[l.name],
[hintField]: lockedBecause?.text || describeLevel(l), [hintField]: lockedBecause?.text || describeLevel(l),
}; };

View file

@ -9,11 +9,12 @@ import {
pickedUpgradesHTMl, pickedUpgradesHTMl,
reasonLevelIsLocked, reasonLevelIsLocked,
} from "./game_utils"; } from "./game_utils";
import { getTotalScore } from "./settings"; import { getSettingValue, getTotalScore } from "./settings";
import { stopRecording } from "./recording"; import { stopRecording } from "./recording";
import { asyncAlert } from "./asyncAlert"; import { asyncAlert } from "./asyncAlert";
import { rawUpgrades } from "./upgrades"; import { rawUpgrades } from "./upgrades";
import { run } from "jest"; import { run } from "jest";
import { editRawLevelList } from "./levelEditor";
export function addToTotalPlayTime(ms: number) { export function addToTotalPlayTime(ms: number) {
try { try {
@ -30,11 +31,18 @@ export function addToTotalPlayTime(ms: number) {
export function gameOver(title: string, intro: string) { export function gameOver(title: string, intro: string) {
if (!gameState.running) return; if (!gameState.running) return;
if (gameState.isGameOver) return; if (gameState.isGameOver) return;
gameState.isGameOver = true; gameState.isGameOver = true;
pause(true); pause(true);
stopRecording(); stopRecording();
addToTotalPlayTime(gameState.runStatistics.runTime); addToTotalPlayTime(gameState.runStatistics.runTime);
if (typeof gameState.isEditorTrialRun === "number") {
editRawLevelList(gameState.isEditorTrialRun);
restart({});
return;
}
// unlocks // unlocks
const endTs = getTotalScore(); const endTs = getTotalScore();
const startTs = endTs - gameState.score; const startTs = endTs - gameState.score;

View file

@ -3,6 +3,29 @@
"confirmRestart.text": "أنت على وشك بدء لعبة جديدة. هل أنت متأكد من رغبتك في المتابعة؟", "confirmRestart.text": "أنت على وشك بدء لعبة جديدة. هل أنت متأكد من رغبتك في المتابعة؟",
"confirmRestart.title": "بدء لعبة جديدة؟", "confirmRestart.title": "بدء لعبة جديدة؟",
"confirmRestart.yes": "إعادة تشغيل اللعبة", "confirmRestart.yes": "إعادة تشغيل اللعبة",
"editor.editing.bigger": "زيادة حجم المستوى",
"editor.editing.color": "اختر لونًا من قائمة الألوان (بحد أقصى 5 لكل مستوى)",
"editor.editing.copy": "نسخ رمز المستوى",
"editor.editing.copy_help": "ألصقه في قناة #levels في Discord الخاص بنا",
"editor.editing.credit": "الاعتمادات والمصدر",
"editor.editing.credit_prompt": "أدخل عنوان URL المصدر أو شرحًا لمستواك.",
"editor.editing.delete": "حذف المستوى",
"editor.editing.down": "انزل كل الطوب إلى الأسفل",
"editor.editing.help": "ثم انقر على البلاط لتلوينه.",
"editor.editing.left": "نقل جميع الطوب إلى اليسار",
"editor.editing.play": "العب هذا المستوى",
"editor.editing.rename": "اسم المستوى",
"editor.editing.rename_prompt": "الرجاء إدخال اسم جديد للمستوى",
"editor.editing.right": "حرك كل الطوب إلى اليمين",
"editor.editing.smaller": "تقليل حجم المستوى",
"editor.editing.title": "مستوى التحرير: {{name}}",
"editor.editing.up": "حرك كل الطوب لأعلى",
"editor.help": "إنشاء مستويات مخصصة ومشاركتها لتضمينها في اللعبة.",
"editor.import": "استيراد المستوى",
"editor.import_instruction": "الصق رمز المستوى لاستيراده في قائمة المستويات الخاصة بك",
"editor.locked": "احصل على مجموع نقاط قدره {{min}} لفتح القفل",
"editor.new_level": "مستوى جديد",
"editor.title": "محرر المستويات",
"gameOver.creative": "لن يتم تسجيل هذا التشغيل.", "gameOver.creative": "لن يتم تسجيل هذا التشغيل.",
"gameOver.cumulative_total": "لقد ارتفع مجموع درجاتك التراكمية من {{startTs}} إلى {{endTs}}.", "gameOver.cumulative_total": "لقد ارتفع مجموع درجاتك التراكمية من {{startTs}} إلى {{endTs}}.",
"gameOver.lost.summary": "لقد أسقطت الكرة بعد التقاط {{score}} قطعة نقدية.", "gameOver.lost.summary": "لقد أسقطت الكرة بعد التقاط {{score}} قطعة نقدية.",

View file

@ -164,6 +164,821 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>editor</name>
<children>
<folder_node>
<name>editing</name>
<children>
<concept_node>
<name>bigger</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>color</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>copy</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>copy_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>credit</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>credit_prompt</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>delete</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>down</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>help</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>left</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>play</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>rename</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>rename_prompt</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>right</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>smaller</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>title</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>up</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<concept_node>
<name>help</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>import</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>import_instruction</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>locked</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>new_level</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>title</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node> <folder_node>
<name>gameOver</name> <name>gameOver</name>
<children> <children>

View file

@ -3,6 +3,29 @@
"confirmRestart.text": "Sie sind dabei, ein neues Spiel zu beginnen. Sind Sie sicher, dass Sie weitermachen wollen?", "confirmRestart.text": "Sie sind dabei, ein neues Spiel zu beginnen. Sind Sie sicher, dass Sie weitermachen wollen?",
"confirmRestart.title": "Ein neues Spiel beginnen?", "confirmRestart.title": "Ein neues Spiel beginnen?",
"confirmRestart.yes": "Spiel neu starten", "confirmRestart.yes": "Spiel neu starten",
"editor.editing.bigger": "Levelgröße erhöhen",
"editor.editing.color": "Wählen Sie eine Farbe aus der Farbliste (max. 5 pro Level)",
"editor.editing.copy": "Levelcode kopieren",
"editor.editing.copy_help": "Fügen Sie es in den Kanal #levels in unserem Discord ein",
"editor.editing.credit": "Credits und Quelle",
"editor.editing.credit_prompt": "Geben Sie die Quell-URL oder Erklärung Ihres Levels ein.",
"editor.editing.delete": "Ebene löschen",
"editor.editing.down": "Bewegen Sie alle Steine nach unten",
"editor.editing.help": "Klicken Sie dann auf eine Kachel, um sie einzufärben.",
"editor.editing.left": "Bewege alle Steine nach links",
"editor.editing.play": "Spiele dieses Level",
"editor.editing.rename": "Ebenenname",
"editor.editing.rename_prompt": "Bitte geben Sie einen neuen Namen für das Level ein",
"editor.editing.right": "Bewege alle Steine nach rechts",
"editor.editing.smaller": "Verringern der Levelgröße",
"editor.editing.title": "Bearbeitungsebene: {{name}}",
"editor.editing.up": "Bewegen Sie alle Steine nach oben",
"editor.help": "Erstellen Sie benutzerdefinierte Level und geben Sie sie frei, um sie in das Spiel aufzunehmen.",
"editor.import": "Importieren einer Ebene",
"editor.import_instruction": "Fügen Sie einen Levelcode ein, um ihn in Ihre Levelliste zu importieren",
"editor.locked": "Erreichen Sie eine Gesamtpunktzahl von {{min}} , um freizuschalten",
"editor.new_level": "Neues Level",
"editor.title": "Level-Editor",
"gameOver.creative": "Dieser Lauf wird nicht aufgezeichnet.", "gameOver.creative": "Dieser Lauf wird nicht aufgezeichnet.",
"gameOver.cumulative_total": "Ihre kumulative Gesamtpunktzahl ist von {{startTs}} auf {{endTs}}gestiegen.", "gameOver.cumulative_total": "Ihre kumulative Gesamtpunktzahl ist von {{startTs}} auf {{endTs}}gestiegen.",
"gameOver.lost.summary": "Du hast den Ball fallen lassen, nachdem du {{score}} Münzen gefangen hast.", "gameOver.lost.summary": "Du hast den Ball fallen lassen, nachdem du {{score}} Münzen gefangen hast.",

View file

@ -3,6 +3,29 @@
"confirmRestart.text": "You're about to start a new game. Are you sure you want to continue?", "confirmRestart.text": "You're about to start a new game. Are you sure you want to continue?",
"confirmRestart.title": "Start a new game?", "confirmRestart.title": "Start a new game?",
"confirmRestart.yes": "Restart game", "confirmRestart.yes": "Restart game",
"editor.editing.bigger": "Increase level size",
"editor.editing.color": "Pick a color in the color list (max 5 per level)",
"editor.editing.copy": "Copy level code",
"editor.editing.copy_help": "Paste it in the #levels channel in our discord",
"editor.editing.credit": "Credits and source",
"editor.editing.credit_prompt": "Enter the source url or explanation of your level.",
"editor.editing.delete": "Delete level",
"editor.editing.down": "Move down all the bricks",
"editor.editing.help": "Then click a tile to color it.",
"editor.editing.left": "Move all bricks to the left",
"editor.editing.play": "Play this level",
"editor.editing.rename": "Level name",
"editor.editing.rename_prompt": "Please enter a new name for the level",
"editor.editing.right": "Move all bricks to the right",
"editor.editing.smaller": "Decrease level size",
"editor.editing.title": "Editing level : {{name}}",
"editor.editing.up": "Move up all the bricks",
"editor.help": "Create custom levels and share them for inclusion in the game.",
"editor.import": "Import a level",
"editor.import_instruction": "Paste a level code to import it in your level list",
"editor.locked": "Reach a total score of {{min}} to unlock",
"editor.new_level": "New level",
"editor.title": "Level Editor",
"gameOver.creative": "This run will not be recorded. ", "gameOver.creative": "This run will not be recorded. ",
"gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.", "gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
"gameOver.lost.summary": "You dropped the ball after catching {{score}} coins.", "gameOver.lost.summary": "You dropped the ball after catching {{score}} coins.",

View file

@ -3,6 +3,29 @@
"confirmRestart.text": "Estás a punto de empezar un nuevo partido: ¿es esto realmente lo que querías?", "confirmRestart.text": "Estás a punto de empezar un nuevo partido: ¿es esto realmente lo que querías?",
"confirmRestart.title": "¿Empezar una nueva partida?", "confirmRestart.title": "¿Empezar una nueva partida?",
"confirmRestart.yes": "Empezar una nueva partida", "confirmRestart.yes": "Empezar una nueva partida",
"editor.editing.bigger": "Aumentar el tamaño del nivel",
"editor.editing.color": "Elige un color de la lista de colores (máximo 5 por nivel)",
"editor.editing.copy": "Copiar código de nivel",
"editor.editing.copy_help": "Pégalo en el canal #levels en nuestro discord",
"editor.editing.credit": "Créditos y fuente",
"editor.editing.credit_prompt": "Introduce la URL de origen o la explicación de tu nivel.",
"editor.editing.delete": "Eliminar nivel",
"editor.editing.down": "Baja todos los ladrillos",
"editor.editing.help": "Luego haz clic en un mosaico para colorearlo.",
"editor.editing.left": "Mueve todos los ladrillos hacia la izquierda",
"editor.editing.play": "Juega este nivel",
"editor.editing.rename": "Nombre del nivel",
"editor.editing.rename_prompt": "Por favor, introduzca un nuevo nombre para el nivel",
"editor.editing.right": "Mueve todos los ladrillos hacia la derecha",
"editor.editing.smaller": "Disminuir el tamaño del nivel",
"editor.editing.title": "Nivel de edición: {{name}}",
"editor.editing.up": "Mueve todos los ladrillos hacia arriba",
"editor.help": "Crea niveles personalizados y compártelos para incluirlos en el juego.",
"editor.import": "Importar un nivel",
"editor.import_instruction": "Pegue un código de nivel para importarlo en su lista de niveles",
"editor.locked": "Alcanza una puntuación total de {{min}} para desbloquear",
"editor.new_level": "Nuevo nivel",
"editor.title": "Editor de niveles",
"gameOver.creative": "Esta parte de la prueba no se grabará.", "gameOver.creative": "Esta parte de la prueba no se grabará.",
"gameOver.cumulative_total": "Su puntuación total acumulada ha pasado de {{startTs}} a {{endTs}}.", "gameOver.cumulative_total": "Su puntuación total acumulada ha pasado de {{startTs}} a {{endTs}}.",
"gameOver.lost.summary": "Se te ha caído la bola después de coger {{score}} monedas.", "gameOver.lost.summary": "Se te ha caído la bola después de coger {{score}} monedas.",

View file

@ -3,6 +3,29 @@
"confirmRestart.text": "Vous êtes sur le point de commencer une nouvelle partie, est-ce vraiment ce que vous vouliez ?", "confirmRestart.text": "Vous êtes sur le point de commencer une nouvelle partie, est-ce vraiment ce que vous vouliez ?",
"confirmRestart.title": "Démarrer une nouvelle partie?", "confirmRestart.title": "Démarrer une nouvelle partie?",
"confirmRestart.yes": "Commencer une nouvelle partie", "confirmRestart.yes": "Commencer une nouvelle partie",
"editor.editing.bigger": "Augmenter la taille du niveau",
"editor.editing.color": "Choisissez une couleur dans la liste des couleurs (max 5 par niveau)",
"editor.editing.copy": "Copier le code du niveau",
"editor.editing.copy_help": "Collez-le dans le canal #levels de notre discord",
"editor.editing.credit": "Crédits et source",
"editor.editing.credit_prompt": "Entrez l'url source ou l'explication de votre niveau.",
"editor.editing.delete": "Supprimer le niveau",
"editor.editing.down": "Déplacez toutes les briques vers le bas",
"editor.editing.help": "Cliquez ensuite sur une tuile pour la colorier.",
"editor.editing.left": "Déplacer toutes les briques vers la gauche",
"editor.editing.play": "Jouez à ce niveau",
"editor.editing.rename": "Nom du niveau",
"editor.editing.rename_prompt": "Veuillez saisir un nouveau nom pour le niveau",
"editor.editing.right": "Déplacer toutes les briques vers la droite",
"editor.editing.smaller": "Diminuer la taille du niveau",
"editor.editing.title": "Niveau d'édition : {{name}}",
"editor.editing.up": "Déplacez toutes les briques",
"editor.help": "Créez des niveaux personnalisés et partagez-les pour les inclure dans le jeu.",
"editor.import": "Importer un niveau",
"editor.import_instruction": "Collez un code de niveau pour l'importer dans votre liste de niveaux",
"editor.locked": "Atteignez un score total de {{min}} pour débloquer",
"editor.new_level": "Nouveau niveau",
"editor.title": "Éditeur de niveau",
"gameOver.creative": "Cette partie de test ne sera pas enregistrée.", "gameOver.creative": "Cette partie de test ne sera pas enregistrée.",
"gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.", "gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
"gameOver.lost.summary": "Vous avez fait tomber la balle après avoir attrapé {{score}} pièces.", "gameOver.lost.summary": "Vous avez fait tomber la balle après avoir attrapé {{score}} pièces.",

View file

@ -3,6 +3,29 @@
"confirmRestart.text": "Вы собираетесь начать новую игру. Вы уверены, что хотите продолжить?", "confirmRestart.text": "Вы собираетесь начать новую игру. Вы уверены, что хотите продолжить?",
"confirmRestart.title": "Начать новую игру?", "confirmRestart.title": "Начать новую игру?",
"confirmRestart.yes": "Перезапустите игру", "confirmRestart.yes": "Перезапустите игру",
"editor.editing.bigger": "Увеличить размер уровня",
"editor.editing.color": "Выберите цвет из списка цветов (максимум 5 на уровень)",
"editor.editing.copy": "Скопировать код уровня",
"editor.editing.copy_help": "Вставьте его в канал #levels в нашем Discord",
"editor.editing.credit": "Кредиты и источник",
"editor.editing.credit_prompt": "Введите исходный URL-адрес или пояснение вашего уровня.",
"editor.editing.delete": "Удалить уровень",
"editor.editing.down": "Сдвиньте все кирпичи вниз.",
"editor.editing.help": "Затем щелкните по плитке, чтобы раскрасить ее.",
"editor.editing.left": "Переместите все кирпичи влево.",
"editor.editing.play": "Пройти этот уровень",
"editor.editing.rename": "Название уровня",
"editor.editing.rename_prompt": "Введите новое название уровня.",
"editor.editing.right": "Переместите все кирпичи вправо.",
"editor.editing.smaller": "Уменьшить размер уровня",
"editor.editing.title": "Уровень редактирования: {{name}}",
"editor.editing.up": "Поднимите все кирпичи.",
"editor.help": "Создавайте собственные уровни и делитесь ими для включения в игру.",
"editor.import": "Импортировать уровень",
"editor.import_instruction": "Вставьте код уровня, чтобы импортировать его в список уровней.",
"editor.locked": "Наберите в общей сложности {{min}} очков, чтобы разблокировать",
"editor.new_level": "Новый уровень",
"editor.title": "Редактор уровней",
"gameOver.creative": "Этот забег не будет записываться.", "gameOver.creative": "Этот забег не будет записываться.",
"gameOver.cumulative_total": "Ваш общий суммарный балл увеличился с {{startTs}} до {{endTs}}.", "gameOver.cumulative_total": "Ваш общий суммарный балл увеличился с {{startTs}} до {{endTs}}.",
"gameOver.lost.summary": "Вы уронили мяч, поймав {{score}} монет.", "gameOver.lost.summary": "Вы уронили мяч, поймав {{score}} монет.",

View file

@ -3,6 +3,29 @@
"confirmRestart.text": "Yeni bir oyuna başlamak üzeresiniz. Devam etmek istediğinizden emin misiniz?", "confirmRestart.text": "Yeni bir oyuna başlamak üzeresiniz. Devam etmek istediğinizden emin misiniz?",
"confirmRestart.title": "Yeni bir oyuna mı başlasam?", "confirmRestart.title": "Yeni bir oyuna mı başlasam?",
"confirmRestart.yes": "Oyunu yeniden başlat", "confirmRestart.yes": "Oyunu yeniden başlat",
"editor.editing.bigger": "Seviye boyutunu artır",
"editor.editing.color": "Renk listesinden bir renk seçin (seviye başına en fazla 5)",
"editor.editing.copy": "Kopyalama seviyesi kodu",
"editor.editing.copy_help": "Bunu Discord'umuzdaki #levels kanalına yapıştırın",
"editor.editing.credit": "Krediler ve kaynak",
"editor.editing.credit_prompt": "Seviyenizin kaynak URL'sini veya açıklamasını girin.",
"editor.editing.delete": "Seviyeyi Sil",
"editor.editing.down": "Tüm tuğlaları aşağı doğru hareket ettirin",
"editor.editing.help": "Daha sonra renklendirmek istediğiniz kutucuğa tıklayın.",
"editor.editing.left": "Tüm tuğlaları sola taşı",
"editor.editing.play": "Bu seviyeyi oyna",
"editor.editing.rename": "Seviye Adı",
"editor.editing.rename_prompt": "Lütfen seviye için yeni bir ad girin",
"editor.editing.right": "Tüm tuğlaları sağa taşı",
"editor.editing.smaller": "Seviye boyutunu azalt",
"editor.editing.title": "Düzenleme düzeyi : {{name}}",
"editor.editing.up": "Tüm tuğlaları yukarı taşı",
"editor.help": "Özel seviyeler yaratın ve bunları oyuna dahil etmek için paylaşın.",
"editor.import": "Bir seviyeyi içe aktar",
"editor.import_instruction": "Seviye listenize aktarmak için bir seviye kodunu yapıştırın",
"editor.locked": "Kilidi açmak için toplam {{min}} puanına ulaşın",
"editor.new_level": "Yeni seviye",
"editor.title": "Seviye Editörü",
"gameOver.creative": "Bu koşu kaydedilmeyecek.", "gameOver.creative": "Bu koşu kaydedilmeyecek.",
"gameOver.cumulative_total": "Toplam kümülatif puanınız {{startTs}} 'dan {{endTs}}'e çıktı.", "gameOver.cumulative_total": "Toplam kümülatif puanınız {{startTs}} 'dan {{endTs}}'e çıktı.",
"gameOver.lost.summary": " {{score}} jeton yakaladıktan sonra topu düşürdün.", "gameOver.lost.summary": " {{score}} jeton yakaladıktan sonra topu düşürdün.",
@ -320,7 +343,7 @@
"upgrades.pierce_color.name": "Renk delme", "upgrades.pierce_color.name": "Renk delme",
"upgrades.pierce_color.tooltip": "+{{lvl}} topun rengindeki tuğlalara hasar", "upgrades.pierce_color.tooltip": "+{{lvl}} topun rengindeki tuğlalara hasar",
"upgrades.pierce_color.verbose_description": "Bir top aynı renkteki bir tuğlaya çarptığında, engellenmeden geçecektir. \n\nFarklı renkteki bir tuğlaya ulaştığında, onu kıracak, rengini alacak ve sekecektir.\n\nSağlam tuğlalarınız varsa, top yine de aynı renkteki bir tuğladan sekebilir.", "upgrades.pierce_color.verbose_description": "Bir top aynı renkteki bir tuğlaya çarptığında, engellenmeden geçecektir. \n\nFarklı renkteki bir tuğlaya ulaştığında, onu kıracak, rengini alacak ve sekecektir.\n\nSağlam tuğlalarınız varsa, top yine de aynı renkteki bir tuğladan sekebilir.",
"upgrades.puck_repulse_ball.help_plural": "", "upgrades.puck_repulse_ball.help_plural": "Daha güçlü itme kuvveti",
"upgrades.puck_repulse_ball.name": "Yumuşak iniş", "upgrades.puck_repulse_ball.name": "Yumuşak iniş",
"upgrades.puck_repulse_ball.tooltip": "Kürek topları iter", "upgrades.puck_repulse_ball.tooltip": "Kürek topları iter",
"upgrades.puck_repulse_ball.verbose_description": "Bir top küreğe yaklaştığında yavaşlamaya başlayacak ve hatta küreğe değmeden bile zıplamaya başlayacaktır.", "upgrades.puck_repulse_ball.verbose_description": "Bir top küreğe yaklaştığında yavaşlamaya başlayacak ve hatta küreğe değmeden bile zıplamaya başlayacaktır.",

292
src/levelEditor.ts Normal file
View file

@ -0,0 +1,292 @@
import { icons, transformRawLevel } from "./loadGameData";
import { t } from "./i18n/i18n";
import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
import { asyncAlert } from "./asyncAlert";
import { Palette, RawLevel } from "./types";
import { levelIconHTML } from "./levelIcon";
import _palette from "./data/palette.json";
import { restart } from "./game";
import { describeLevel } from "./game_utils";
const palette = _palette as Palette;
export function levelEditorMenuEntry() {
const min = 10000;
const disabled = getTotalScore() < min;
return {
icon: icons["icon:editor"],
text: t("editor.title"),
disabled,
help: disabled ? t("editor.locked", { min }) : t("editor.help"),
async value() {
openLevelEditorLevelsList().then();
},
};
}
async function openLevelEditorLevelsList() {
const rawList = getSettingValue("custom_levels", []) as RawLevel[];
const customLevels = rawList.map(transformRawLevel);
let choice = await asyncAlert({
title: t("editor.title"),
content: [
...customLevels.map((l, li) => ({
text: l.name,
icon: levelIconHTML(l.bricks, l.size, l.color),
value() {
editRawLevelList(li);
},
help: l.credit || describeLevel(l),
})),
{
text: t("editor.new_level"),
icon: icons["icon:editor"],
value() {
rawList.push({
color: "",
size: 6,
bricks: "____________________________________",
name: "custom level" + (rawList.length + 1),
credit: "",
});
setSettingValue("custom_levels", rawList);
editRawLevelList(rawList.length - 1);
},
},
{
text: t("editor.import"),
help: t("editor.import_instruction"),
value() {
const code = prompt(t("editor.import_instruction"))?.trim();
if (code) {
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 >= 2 && size <= 20) {
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") {
let rawList = getSettingValue("custom_levels", []) as RawLevel[];
const level = rawList[nth];
const bricks = level.bricks.split("");
let grid = "";
for (let y = 0; y < level.size; y++) {
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"),
disabled:
!level.name ||
!level.credit ||
bricks.filter((b) => b !== "_").length < 6,
},
{
text: t("editor.editing.bigger"),
value: "size:+1",
disabled: level.size > 20,
},
{
text: t("editor.editing.smaller"),
value: "size:-1",
disabled: level.size < 3,
},
{
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?.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);
}
function automaticBackgroundColor(bricks: string[]) {
return bricks.filter((b) => b === "g").length >
bricks.filter((b) => b !== "_").length * 0.05
? "#115988"
: "";
}

View file

@ -13,28 +13,29 @@ const rawLevelsList = _rawLevelsList as RawLevel[];
export const appVersion = _appVersion as string; export const appVersion = _appVersion as string;
export const icons = {} as { [k: string]: string }; export const icons = {} as { [k: string]: string };
export const allLevelsAndIcons = rawLevelsList
.map((level, i) => { export function transformRawLevel(level: RawLevel) {
const bricks = level.bricks const bricks = level.bricks
.split("") .split("")
.map((c) => palette[c]) .map((c) => palette[c])
.slice(0, level.size * level.size); .slice(0, level.size * level.size);
const bricksCount = bricks.filter((i) => i).length; const bricksCount = bricks.filter((i) => i).length;
const icon = levelIconHTML(bricks, level.size, level.color); const icon = levelIconHTML(bricks, level.size, level.color);
icons[level.name] = icon; icons[level.name] = icon;
return { return {
...level, ...level,
bricks, bricks,
bricksCount, bricksCount,
icon, icon,
color: level.color || "#000000", color: level.color || "#000000",
svg: getLevelBackground(level), svg: getLevelBackground(level),
}; sortKey: ((Math.random() + 3) / 3.5) * bricksCount,
}) };
.map((l, li) => ({ }
...l,
sortKey: ((Math.random() + 3) / 3.5) * l.bricksCount, export const allLevelsAndIcons = rawLevelsList.map(
})) as Level[]; transformRawLevel,
) as Level[];
export const allLevels = allLevelsAndIcons.filter( export const allLevels = allLevelsAndIcons.filter(
(l) => !l.name.startsWith("icon:"), (l) => !l.name.startsWith("icon:"),

View file

@ -28,12 +28,12 @@ export function getRunLevels(
(l, li) => (l, li) =>
unlockedBefore.has(l.name) || !reasonLevelIsLocked(li, history, false), unlockedBefore.has(l.name) || !reasonLevelIsLocked(li, history, false),
); );
const firstLevel = allLevelsAndIcons.filter( const firstLevel = params?.level
(l) => l.name == (params?.level || "icon:" + randomGift), ? [params.level]
); : allLevelsAndIcons.filter((l) => l.name == "icon:" + randomGift);
const restInRandomOrder = unlocked const restInRandomOrder = unlocked
.filter((l) => l.name !== params?.level) .filter((l) => l.name !== params?.level?.name)
.filter((l) => l.name !== params?.levelToAvoid) .filter((l) => l.name !== params?.levelToAvoid)
.sort(() => Math.random() - 0.5); .sort(() => Math.random() - 0.5);
@ -141,8 +141,9 @@ export function newGameState(params: RunParams): GameState {
creative: creative:
params?.computer_controlled || params?.computer_controlled ||
sumOfValues(params.perks) > 1 || sumOfValues(params.perks) > 1 ||
(params.level && !params.level.startsWith("icon:")), (params.level && !params.level.name.startsWith("icon:")),
computer_controlled: params?.computer_controlled || false, computer_controlled: params?.computer_controlled || false,
isEditorTrialRun: params?.isEditorTrialRun,
}; };
resetBalls(gameState); resetBalls(gameState);

5
src/types.d.ts vendored
View file

@ -7,7 +7,6 @@ export type RawLevel = {
name: string; name: string;
size: number; size: number;
bricks: string; bricks: string;
svg: number | null;
color: string; color: string;
credit?: string; credit?: string;
}; };
@ -282,14 +281,16 @@ export type GameState = {
rerolls: number; rerolls: number;
creative: boolean; creative: boolean;
computer_controlled: boolean; computer_controlled: boolean;
isEditorTrialRun?: number;
}; };
export type RunParams = { export type RunParams = {
level?: string; level?: Level;
levelToAvoid?: string; levelToAvoid?: string;
perkToAvoid?: PerkId; perkToAvoid?: PerkId;
perks?: Partial<PerksMap>; perks?: Partial<PerksMap>;
computer_controlled?: boolean; computer_controlled?: boolean;
isEditorTrialRun?: number;
}; };
export type OptionDef = { export type OptionDef = {
default: boolean; default: boolean;