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" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29088937 versionCode = 29090246
versionName = "29088937" versionName = "29090246"
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

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. // The version of the cache.
const VERSION = "29088937"; const VERSION = "29090246";
// 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,4 +1,5 @@
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import { isOptionOn } from "./options";
export let alertsOpen = 0, export let alertsOpen = 0,
closeModal: null | (() => void) = null; closeModal: null | (() => void) = null;
@ -118,9 +119,6 @@ ${icon}
<em>${help || ""}</em> <em>${help || ""}</em>
</div>`; </div>`;
if (tooltip) {
button.setAttribute("data-tooltip", tooltip);
}
if (disabled) { if (disabled) {
button.setAttribute("disabled", "disabled"); button.setAttribute("disabled", "disabled");
} else { } else {
@ -137,6 +135,31 @@ ${icon}
className + (lastClickedItemIndex === index ? " needs-focus" : ""); className + (lastClickedItemIndex === index ? " needs-focus" : "");
addto.appendChild(button); 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( popup.addEventListener(

View file

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

View file

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

View file

@ -197,6 +197,7 @@ body:not(.has-alert-open) #popup {
&.actionsAsGrid.large > div > section { &.actionsAsGrid.large > div > section {
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
} }
&.actionsAsGrid > div { &.actionsAsGrid > div {
max-width: none; max-width: none;
@ -504,10 +505,12 @@ h2.histogram-title strong {
opacity 200ms, opacity 200ms,
transform 200ms; transform 200ms;
z-index: 7; z-index: 7;
&.hidden { &.hidden {
opacity: 0; opacity: 0;
transform: translate(-20px, -20px) scale(0.5); transform: translate(-20px, -20px) scale(0.5);
} }
&.visible { &.visible {
opacity: 0.8; opacity: 0.8;
transform: none; transform: none;
@ -523,6 +526,7 @@ h2.histogram-title strong {
height: 40px; height: 40px;
border: 1px solid; border: 1px solid;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
border-color: gold; border-color: gold;
z-index: 1; z-index: 1;
@ -530,6 +534,7 @@ h2.histogram-title strong {
box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.2); box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.2);
} }
} }
.gridEdit { .gridEdit {
& > div { & > div {
display: flex; display: flex;
@ -544,6 +549,7 @@ h2.histogram-title strong {
.palette { .palette {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
& > span { & > span {
&[data-selected="true"] { &[data-selected="true"] {
border: 2px solid white; border: 2px solid white;
@ -561,15 +567,18 @@ h2.histogram-title strong {
z-index: 3; z-index: 3;
pointer-events: none; pointer-events: none;
opacity: 1; opacity: 1;
& > div { & > div {
background: rgba(38, 38, 38, 0.5); background: rgba(38, 38, 38, 0.5);
position: relative; position: relative;
> div { > div {
background: @purple; background: @purple;
position: absolute; position: absolute;
inset: 0; inset: 0;
transform-origin: top left; transform-origin: top left;
} }
> strong { > strong {
position: relative; position: relative;
padding: 0 5px; padding: 0 5px;
@ -579,6 +588,7 @@ h2.histogram-title strong {
.highlight { .highlight {
position: relative; position: relative;
&:before { &:before {
content: ""; content: "";
position: absolute; position: absolute;
@ -588,7 +598,18 @@ h2.histogram-title strong {
opacity: 0.3; opacity: 0.3;
} }
} }
.not-highlighed { .not-highlighed {
opacity: 0.8; opacity: 0.8;
color: #8a8a8a; 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, catchRateGood,
clamp, clamp,
levelTimeBest, levelTimeBest,
levelTimeGood, miniMarkDown, levelTimeGood,
miniMarkDown,
missesBest, missesBest,
missesGood, missesGood,
wallBouncedBest, wallBouncedBest,
@ -97,7 +98,7 @@ 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"; import { levelEditorMenuEntry } from "./levelEditor";
import {categories} from "./upgrades"; import { categories } from "./upgrades";
export async function play() { export async function play() {
if (await applyFullScreenChoice()) return; if (await applyFullScreenChoice()) return;
@ -912,7 +913,7 @@ async function openUnlockedUpgradesList() {
const ts = getTotalScore(); const ts = getTotalScore();
const upgradeActions = upgrades const upgradeActions = upgrades
.sort((a, b) => a.threshold - b.threshold) .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, text: name,
disabled: ts < threshold, disabled: ts < threshold,
value: { value: {
@ -925,7 +926,7 @@ async function openUnlockedUpgradesList() {
ts < threshold ts < threshold
? t("unlocks.minTotalScore", { score: threshold }) ? t("unlocks.minTotalScore", { score: threshold })
: help(1), : help(1),
tooltip:ts < threshold ? '': fullHelp, tooltip: ts < threshold ? "" : fullHelp,
})); }));
const tryOn = await asyncAlert<RunParams>({ const tryOn = await asyncAlert<RunParams>({
@ -935,17 +936,19 @@ async function openUnlockedUpgradesList() {
}), }),
content: [ content: [
t("unlocks.intro", { ts }), t("unlocks.intro", { ts }),
upgradeActions.find((u) => u.disabled) ? t("unlocks.greyed_out_help") : "", upgradeActions.find((u) => u.disabled)
? t("unlocks.greyed_out_help")
: "",
miniMarkDown(t("unlocks.category.beginner")), miniMarkDown(t("unlocks.category.beginner")),
...upgradeActions.filter(u=>u.category==categories.beginner), ...upgradeActions.filter((u) => u.category == categories.beginner),
miniMarkDown(t("unlocks.category.combo")), miniMarkDown(t("unlocks.category.combo")),
...upgradeActions.filter(u=>u.category==categories.combo), ...upgradeActions.filter((u) => u.category == categories.combo),
miniMarkDown(t("unlocks.category.combo_boost")), miniMarkDown(t("unlocks.category.combo_boost")),
...upgradeActions.filter(u=>u.category==categories.combo_boost), ...upgradeActions.filter((u) => u.category == categories.combo_boost),
miniMarkDown(t("unlocks.category.simple")), miniMarkDown(t("unlocks.category.simple")),
...upgradeActions.filter(u=>u.category==categories.simple), ...upgradeActions.filter((u) => u.category == categories.simple),
miniMarkDown(t("unlocks.category.advanced")), miniMarkDown(t("unlocks.category.advanced")),
...upgradeActions.filter(u=>u.category==categories.advanced), ...upgradeActions.filter((u) => u.category == categories.advanced),
], ],
allowClose: true, allowClose: true,
// className: "actionsAsGrid large", // className: "actionsAsGrid large",

View file

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

View file

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

View file

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

View file

@ -40,7 +40,7 @@ async function openLevelEditorLevelsList() {
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),
value() { value() {
editRawLevelList(li); editRawLevelList(li);
}, },

View file

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

View file

@ -9,7 +9,6 @@ import {
levelCodeToRawLevel, levelCodeToRawLevel,
} from "../pure_functions"; } from "../pure_functions";
const palette = _palette as Palette; const palette = _palette as Palette;
let allLevels = null; let allLevels = null;
@ -22,13 +21,22 @@ function App() {
fetch("http://localhost:4400/src/data/levels.json") fetch("http://localhost:4400/src/data/levels.json")
.then((r) => r.json()) .then((r) => r.json())
.then((lvls) => { .then((lvls) => {
const cleaned = lvls.map((l) => ({
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||''})) 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 = [ const sorted = [
...cleaned.filter(l=>l.name.match('icon:')).sort((a,b)=>a.name>b.name ? 1:-1), ...cleaned
...cleaned.filter(l=>!l.name.match('icon:')) .filter((l) => l.name.match("icon:"))
] .sort((a, b) => (a.name > b.name ? 1 : -1)),
setLevels(sorted as RawLevel[]) ...cleaned.filter((l) => !l.name.match("icon:")),
];
setLevels(sorted as RawLevel[]);
allLevels = sorted; allLevels = sorted;
}); });
}, []); }, []);

View file

@ -30,18 +30,6 @@ describe("json data checks", () => {
it("Has a few colors", () => { it("Has a few colors", () => {
expect(Object.keys(_palette).length).toBeGreaterThan(10); 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", () => { it("Has an _appVersion", () => {
expect(parseInt(_appVersion)).toBeGreaterThan(2000); expect(parseInt(_appVersion)).toBeGreaterThan(2000);
}); });

View file

@ -8,6 +8,7 @@ import { levelIconHTML } from "./levelIcon";
import { automaticBackgroundColor } from "./pure_functions"; 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 palette = _palette as Palette;
const rawLevelsList = _rawLevelsList as RawLevel[]; const rawLevelsList = _rawLevelsList as RawLevel[];
@ -22,7 +23,7 @@ export function transformRawLevel(level: RawLevel) {
.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);
icons[level.name] = icon; icons[level.name] = icon;
return { return {
...level, ...level,
@ -43,7 +44,3 @@ export const allLevels = allLevelsAndIcons.filter(
(l) => !l.name.startsWith("icon:"), (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 { getHistory } from "./gameOver";
import { appVersion, icons } from "./loadGameData"; import {appVersion, icons, upgrades} from "./loadGameData";
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import { asyncAlert } from "./asyncAlert"; import { asyncAlert } from "./asyncAlert";
import { rawUpgrades } from "./upgrades";
import { getSettingValue, setSettingValue } from "./settings"; import { getSettingValue, setSettingValue } from "./settings";
export function runHistoryViewerMenuEntry() { export function runHistoryViewerMenuEntry() {
@ -31,7 +30,7 @@ export function runHistoryViewerMenuEntry() {
label: t("history.columns.score"), label: t("history.columns.score"),
field: (r) => r.score, field: (r) => r.score,
}, },
...rawUpgrades.map((u) => ({ ...upgrades.map((u) => ({
label: icons["icon:" + u.id], label: icons["icon:" + u.id],
tooltip: u.name, tooltip: u.name,
field: (r) => r.perks?.[u.id] || 0, field: (r) => r.perks?.[u.id] || 0,

View file

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

4
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;
color: string;
credit?: string; credit?: string;
}; };
export type Level = { export type Level = {
@ -28,8 +27,7 @@ export type Upgrade = {
gift: boolean; gift: boolean;
id: PerkId; id: PerkId;
name: string; name: string;
category: string; category: number;
icon: string;
max: number; max: number;
help: (lvl: number) => string; help: (lvl: number) => string;
fullHelp: string; fullHelp: string;

View file

@ -1,7 +1,7 @@
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import { comboKeepingRate } from "./pure_functions"; import { comboKeepingRate } from "./pure_functions";
import { PerkId } from "./types"; import {PerkId, Upgrade} from "./types";
// Those perks are excluded from creative mode // Those perks are excluded from creative mode
export const noCreative: PerkId[] = [ export const noCreative: PerkId[] = [
@ -10,13 +10,13 @@ export const noCreative: PerkId[] = [
"one_more_choice", "one_more_choice",
]; ];
export const categories={ export const categories = {
beginner:1, beginner: 1,
combo:2, combo: 2,
combo_boost:2.5, combo_boost: 2.5,
simple:3, simple: 3,
advanced:4, advanced: 4,
} };
export const rawUpgrades = [ export const rawUpgrades = [
{ {
@ -314,7 +314,8 @@ export const rawUpgrades = [
id: "asceticism", id: "asceticism",
max: 1, max: 1,
name: t("upgrades.asceticism.name"), name: t("upgrades.asceticism.name"),
help: (lvl: number) => t("upgrades.asceticism.tooltip", { combo: lvl * 3 }), help: (lvl: number) =>
t("upgrades.asceticism.tooltip", { combo: lvl * 3 }),
fullHelp: t("upgrades.asceticism.verbose_description"), fullHelp: t("upgrades.asceticism.verbose_description"),
}, },
// Regular // Regular
@ -349,7 +350,8 @@ export const rawUpgrades = [
gift: true, gift: true,
max: 6, max: 6,
name: t("upgrades.multiball.name"), name: t("upgrades.multiball.name"),
help: (lvl: number) => t("upgrades.multiball.tooltip", { count: lvl + 1 }), help: (lvl: number) =>
t("upgrades.multiball.tooltip", { count: lvl + 1 }),
fullHelp: t("upgrades.multiball.verbose_description"), fullHelp: t("upgrades.multiball.verbose_description"),
}, },
{ {
@ -425,7 +427,8 @@ export const rawUpgrades = [
id: "bricks_attract_coins", id: "bricks_attract_coins",
max: 3, max: 3,
name: t("upgrades.bricks_attract_coins.name"), name: t("upgrades.bricks_attract_coins.name"),
help: (lvl: number) => t("upgrades.bricks_attract_coins.tooltip", { lvl }), help: (lvl: number) =>
t("upgrades.bricks_attract_coins.tooltip", { lvl }),
fullHelp: t("upgrades.bricks_attract_coins.verbose_description"), fullHelp: t("upgrades.bricks_attract_coins.verbose_description"),
}, },
{ {
@ -685,7 +688,8 @@ export const rawUpgrades = [
id: "buoy", id: "buoy",
max: 3, max: 3,
name: t("upgrades.buoy.name"), name: t("upgrades.buoy.name"),
help: (lvl: number) => t("upgrades.buoy.tooltip", { duration: lvl * 0.5 }), help: (lvl: number) =>
t("upgrades.buoy.tooltip", { duration: lvl * 0.5 }),
fullHelp: t("upgrades.buoy.verbose_description"), fullHelp: t("upgrades.buoy.verbose_description"),
}, },
{ {
@ -883,4 +887,5 @@ export const rawUpgrades = [
t("upgrades.sturdy_bricks.tooltip", { lvl, percent: lvl * 50 }), t("upgrades.sturdy_bricks.tooltip", { lvl, percent: lvl * 50 }),
fullHelp: t("upgrades.sturdy_bricks.verbose_description"), fullHelp: t("upgrades.sturdy_bricks.verbose_description"),
}, },
].sort((a,b)=>(a.category-b.category) || (a.threshold-b.threshold)) as const; ] as const