This commit is contained in:
Renan LE CARO 2025-04-23 15:37:07 +02:00
parent cb90fef3a9
commit 401e9b4548
22 changed files with 1024 additions and 959 deletions

View file

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

File diff suppressed because one or more lines are too long

83
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.
const VERSION = "29088937";
const VERSION = "29090246";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -1,4 +1,5 @@
import { t } from "./i18n/i18n";
import { isOptionOn } from "./options";
export let alertsOpen = 0,
closeModal: null | (() => void) = null;
@ -118,9 +119,6 @@ ${icon}
<em>${help || ""}</em>
</div>`;
if (tooltip) {
button.setAttribute("data-tooltip", tooltip);
}
if (disabled) {
button.setAttribute("disabled", "disabled");
} else {
@ -137,6 +135,31 @@ ${icon}
className + (lastClickedItemIndex === index ? " needs-focus" : "");
addto.appendChild(button);
if (tooltip) {
if (isOptionOn("mobile-mode")) {
const helpBtn = document.createElement("button");
helpBtn.innerText = "?";
helpBtn.className = "help";
let helpTooltip = document.createElement("div");
helpTooltip.textContent = tooltip;
helpTooltip.className = "help_button_tooltip";
helpBtn.addEventListener("click", (e) => {
e.stopPropagation();
});
helpBtn.addEventListener("touchstart", (e) => {
e.stopPropagation();
document.body.appendChild(helpTooltip);
});
helpBtn.addEventListener("touchend", (e) => {
document.body.removeChild(helpTooltip);
});
button.appendChild(helpBtn);
} else {
button.setAttribute("data-tooltip", tooltip);
}
}
});
popup.addEventListener(

View file

@ -57,7 +57,7 @@ export async function openCreativeModePerksPicker() {
};
}),
...customLevels.map((l) => ({
icon: levelIconHTML(l.bricks, l.size, l.color),
icon: levelIconHTML(l.bricks, l.size),
text: l.name,
value: l,
disabled: !l.bricks.filter((b) => b !== "_").length,
@ -93,7 +93,7 @@ export async function openCreativeModePerksPicker() {
...upgrades
.filter((u) => !noCreative.includes(u.id))
.map((u) => ({
icon: u.icon,
icon: icons['icon:'+u.id],
text: u.name,
help:
(creativeModePerks[u.id] || 0) +

View file

@ -1397,4 +1397,4 @@
"bricks": "____________________________gggg______________ggrrrrgg___________grrrrrrrrg_________grrrrrrrrrrg_______grrrWWrrrrWWrg______grrWWWWrrWWWWg______grrWWbbrrWWbbg_____grrrWWbbrrWWbbrg____grrrrWWrrrrWWrrg____grrrrrrrrrrrrrrg____grrrrrrrrrrrrrrg____grrrrrrrrrrrrrrg____grrrrrrrrrrrrrrg____grrgrrrggrrrgrrg____grg_grg__grg_grg_____g___g____g___g_______________________________________________________________",
"credit": "Suggested by Big Goober. The red ghost, Blinky, from the arcade game \"Pac Man\""
}
]
]

View file

@ -1 +1 @@
"29088937"
"29090246"

View file

@ -197,6 +197,7 @@ body:not(.has-alert-open) #popup {
&.actionsAsGrid.large > div > section {
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
}
&.actionsAsGrid > div {
max-width: none;
@ -504,10 +505,12 @@ h2.histogram-title strong {
opacity 200ms,
transform 200ms;
z-index: 7;
&.hidden {
opacity: 0;
transform: translate(-20px, -20px) scale(0.5);
}
&.visible {
opacity: 0.8;
transform: none;
@ -523,6 +526,7 @@ h2.histogram-title strong {
height: 40px;
border: 1px solid;
cursor: pointer;
&:hover {
border-color: gold;
z-index: 1;
@ -530,6 +534,7 @@ h2.histogram-title strong {
box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.2);
}
}
.gridEdit {
& > div {
display: flex;
@ -544,6 +549,7 @@ h2.histogram-title strong {
.palette {
display: flex;
flex-wrap: wrap;
& > span {
&[data-selected="true"] {
border: 2px solid white;
@ -561,15 +567,18 @@ h2.histogram-title strong {
z-index: 3;
pointer-events: none;
opacity: 1;
& > div {
background: rgba(38, 38, 38, 0.5);
position: relative;
> div {
background: @purple;
position: absolute;
inset: 0;
transform-origin: top left;
}
> strong {
position: relative;
padding: 0 5px;
@ -579,6 +588,7 @@ h2.histogram-title strong {
.highlight {
position: relative;
&:before {
content: "";
position: absolute;
@ -588,7 +598,18 @@ h2.histogram-title strong {
opacity: 0.3;
}
}
.not-highlighed {
opacity: 0.8;
color: #8a8a8a;
}
.help_button_tooltip {
position: fixed;
z-index: 8;
left: 0;
top: 0;
background: black;
padding: 20px;
border-bottom: 1px solid white;
}

View file

@ -80,7 +80,8 @@ import {
catchRateGood,
clamp,
levelTimeBest,
levelTimeGood, miniMarkDown,
levelTimeGood,
miniMarkDown,
missesBest,
missesGood,
wallBouncedBest,
@ -97,7 +98,7 @@ import { runHistoryViewerMenuEntry } from "./runHistoryViewer";
import { getNearestUnlockHTML, openScorePanel } from "./openScorePanel";
import { monitorLevelsUnlocks } from "./monitorLevelsUnlocks";
import { levelEditorMenuEntry } from "./levelEditor";
import {categories} from "./upgrades";
import { categories } from "./upgrades";
export async function play() {
if (await applyFullScreenChoice()) return;
@ -912,7 +913,7 @@ async function openUnlockedUpgradesList() {
const ts = getTotalScore();
const upgradeActions = upgrades
.sort((a, b) => a.threshold - b.threshold)
.map(({ name, id, threshold, icon, help ,category, fullHelp}) => ({
.map(({ name, id, threshold, icon, help, category, fullHelp }) => ({
text: name,
disabled: ts < threshold,
value: {
@ -925,7 +926,7 @@ async function openUnlockedUpgradesList() {
ts < threshold
? t("unlocks.minTotalScore", { score: threshold })
: help(1),
tooltip:ts < threshold ? '': fullHelp,
tooltip: ts < threshold ? "" : fullHelp,
}));
const tryOn = await asyncAlert<RunParams>({
@ -934,18 +935,20 @@ async function openUnlockedUpgradesList() {
out_of: upgradeActions.length,
}),
content: [
t("unlocks.intro", { ts }),
upgradeActions.find((u) => u.disabled) ? t("unlocks.greyed_out_help") : "",
miniMarkDown(t("unlocks.category.beginner")),
...upgradeActions.filter(u=>u.category==categories.beginner),
miniMarkDown(t("unlocks.category.combo")),
...upgradeActions.filter(u=>u.category==categories.combo),
miniMarkDown(t("unlocks.category.combo_boost")),
...upgradeActions.filter(u=>u.category==categories.combo_boost),
miniMarkDown(t("unlocks.category.simple")),
...upgradeActions.filter(u=>u.category==categories.simple),
miniMarkDown(t("unlocks.category.advanced")),
...upgradeActions.filter(u=>u.category==categories.advanced),
t("unlocks.intro", { ts }),
upgradeActions.find((u) => u.disabled)
? t("unlocks.greyed_out_help")
: "",
miniMarkDown(t("unlocks.category.beginner")),
...upgradeActions.filter((u) => u.category == categories.beginner),
miniMarkDown(t("unlocks.category.combo")),
...upgradeActions.filter((u) => u.category == categories.combo),
miniMarkDown(t("unlocks.category.combo_boost")),
...upgradeActions.filter((u) => u.category == categories.combo_boost),
miniMarkDown(t("unlocks.category.simple")),
...upgradeActions.filter((u) => u.category == categories.simple),
miniMarkDown(t("unlocks.category.advanced")),
...upgradeActions.filter((u) => u.category == categories.advanced),
],
allowClose: true,
// className: "actionsAsGrid large",

View file

@ -16,7 +16,6 @@ import {
} from "./settings";
import { stopRecording } from "./recording";
import { asyncAlert } from "./asyncAlert";
import { rawUpgrades } from "./upgrades";
import { editRawLevelList } from "./levelEditor";
import { openCreativeModePerksPicker } from "./creative";
@ -52,7 +51,7 @@ export function gameOver(title: string, intro: string) {
// unlocks
const endTs = getTotalScore();
const startTs = endTs - gameState.score;
const unlockedPerks = rawUpgrades.filter(
const unlockedPerks = upgrades.filter(
(o) => o.threshold > startTs && o.threshold < endTs,
);

View file

@ -11,7 +11,6 @@ import {
import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { clamp } from "./pure_functions";
import { rawUpgrades } from "./upgrades";
import { hashCode } from "./getLevelBackground";
import { getSettingValue, getTotalScore } from "./settings";
import { isOptionOn } from "./options";
@ -132,7 +131,7 @@ export function pickedUpgradesHTMl(gameState: GameState) {
state,
html: `
<div class="upgrade ${["??", "used", "banned", "free"][state]}">
${u.icon}
${icons['icon:'+u.id]}
<p>
<strong>${u.name}</strong>
${u.help(Math.max(1, gameState.perks[u.id]))}
@ -310,7 +309,7 @@ function isExcluded(id: PerkId) {
"slow_down",
]);
// Avoid excluding a perk that's needed for the required one
rawUpgrades.forEach((u) => {
upgrades.forEach((u) => {
if (u.requires) excluded.add(u.requires);
});
}
@ -323,7 +322,7 @@ export function getLevelUnlockCondition(levelIndex: number) {
minScore = Math.max(-1000 + 100 * levelIndex, 0);
if (levelIndex > 20) {
const possibletargets = [...rawUpgrades]
const possibletargets = [...upgrades]
.slice(0, Math.floor(levelIndex / 2))
.filter((u) => !isExcluded(u.id))
.sort(

View file

@ -39,7 +39,7 @@ export function helpMenuEntry() {
...upgrades.map(
(u) => `
<div class="upgrade used">
${u.icon}
${icons['icon:'+u.id]}
<p>
<strong>${u.name}</strong><br/>
${u.help(1)}

View file

@ -40,7 +40,7 @@ async function openLevelEditorLevelsList() {
content: [
...customLevels.map((l, li) => ({
text: l.name,
icon: levelIconHTML(l.bricks, l.size, l.color),
icon: levelIconHTML(l.bricks, l.size),
value() {
editRawLevelList(li);
},

View file

@ -10,7 +10,6 @@ const levelIconHTMLCanvasCtx =
export function levelIconHTML(
bricks: string[],
levelSize: number,
color: string,
) {
const size = 46;
const c = levelIconHTMLCanvas;

View file

@ -2,14 +2,13 @@ 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 { 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;
@ -22,13 +21,22 @@ function App() {
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[])
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;
});
}, []);

View file

@ -30,18 +30,6 @@ describe("json data checks", () => {
it("Has a few colors", () => {
expect(Object.keys(_palette).length).toBeGreaterThan(10);
});
it("Avoids dark bricks on dark bg", () => {
const levelsWithDarkBricksAndBG = _rawLevelsList
.filter((l) => !l.color && !l.name.match(/^icon:/))
.map((l) => ({
name: l.name,
bricks: l.bricks.split("").filter((c) => c !== "_").length,
darkBricks: l.bricks.split("").filter((c) => c === "g").length,
}))
.filter((l) => l.darkBricks > 0.05 * l.bricks);
expect(levelsWithDarkBricksAndBG).toEqual([]);
});
it("Has an _appVersion", () => {
expect(parseInt(_appVersion)).toBeGreaterThan(2000);
});

View file

@ -8,6 +8,7 @@ import { levelIconHTML } from "./levelIcon";
import { automaticBackgroundColor } from "./pure_functions";
export const upgrades = [...rawUpgrades].sort((a, b) => a.category - b.category || a.threshold - b.threshold) as Upgrade[];
const palette = _palette as Palette;
const rawLevelsList = _rawLevelsList as RawLevel[];
@ -22,7 +23,7 @@ export function transformRawLevel(level: RawLevel) {
.map((c) => palette[c])
.slice(0, level.size * level.size);
const bricksCount = bricks.filter((i) => i).length;
const icon = levelIconHTML(bricks, level.size, level.color);
const icon = levelIconHTML(bricks, level.size);
icons[level.name] = icon;
return {
...level,
@ -43,7 +44,3 @@ export const allLevels = allLevelsAndIcons.filter(
(l) => !l.name.startsWith("icon:"),
);
export const upgrades = rawUpgrades.map((u) => ({
...u,
icon: icons["icon:" + u.id],
})) as Upgrade[];

View file

@ -1,8 +1,7 @@
import { getHistory } from "./gameOver";
import { appVersion, icons } from "./loadGameData";
import {appVersion, icons, upgrades} from "./loadGameData";
import { t } from "./i18n/i18n";
import { asyncAlert } from "./asyncAlert";
import { rawUpgrades } from "./upgrades";
import { getSettingValue, setSettingValue } from "./settings";
export function runHistoryViewerMenuEntry() {
@ -31,7 +30,7 @@ export function runHistoryViewerMenuEntry() {
label: t("history.columns.score"),
field: (r) => r.score,
},
...rawUpgrades.map((u) => ({
...upgrades.map((u) => ({
label: icons["icon:" + u.id],
tooltip: u.name,
field: (r) => r.perks?.[u.id] || 0,

View file

@ -31,7 +31,7 @@ export async function openStartingPerksEditor() {
const buttons = avaliable.map((u) => {
const checked = isStartingPerk(u);
return {
icon: u.icon,
icon: icons['icon:'+u.id],
text: u.name,
tooltip: u.help(1),
value: [u],

4
src/types.d.ts vendored
View file

@ -7,7 +7,6 @@ export type RawLevel = {
name: string;
size: number;
bricks: string;
color: string;
credit?: string;
};
export type Level = {
@ -28,8 +27,7 @@ export type Upgrade = {
gift: boolean;
id: PerkId;
name: string;
category: string;
icon: string;
category: number;
max: number;
help: (lvl: number) => string;
fullHelp: string;

File diff suppressed because it is too large Load diff