mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-25 14:36:15 -04:00
wip
This commit is contained in:
parent
9624c5b351
commit
e78021ff83
24 changed files with 840 additions and 437 deletions
|
@ -1,5 +1,5 @@
|
|||
// The version of the cache.
|
||||
const VERSION = "29065815";
|
||||
const VERSION = "29067102";
|
||||
|
||||
// The name of the cache
|
||||
const CACHE_NAME = `breakout-71-${VERSION}`;
|
||||
|
|
|
@ -43,7 +43,7 @@ export async function asyncAlert<t>({
|
|||
content: (string | AsyncAlertAction<t>)[];
|
||||
allowClose?: boolean;
|
||||
className?: string;
|
||||
}): Promise<t | void|string> {
|
||||
}): Promise<t | void> {
|
||||
updateAlertsOpen(+1);
|
||||
return new Promise((resolve) => {
|
||||
popupWrap.className = className;
|
||||
|
@ -139,11 +139,16 @@ ${icon}
|
|||
addto.appendChild(button);
|
||||
});
|
||||
|
||||
popup.addEventListener('click', e=>{
|
||||
if(e.target.getAttribute('data-resolve-to')){
|
||||
closeWithResult(e.target.getAttribute('data-resolve-to'))
|
||||
popup.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.getAttribute("data-resolve-to")) {
|
||||
closeWithResult(target.getAttribute("data-resolve-to") as t);
|
||||
}
|
||||
},true)
|
||||
},
|
||||
true,
|
||||
);
|
||||
popupWrap.appendChild(popup);
|
||||
(
|
||||
popupWrap.querySelector(
|
||||
|
|
|
@ -75,7 +75,8 @@ export async function openCreativeModePerksPicker() {
|
|||
})),
|
||||
t("lab.select_level"),
|
||||
...allLevels.map((l, li) => {
|
||||
const problem = reasonLevelIsLocked(li, getHistory());
|
||||
const problem =
|
||||
reasonLevelIsLocked(li, getHistory(), true)?.text || "";
|
||||
return {
|
||||
icon: icons[l.name],
|
||||
text: l.name,
|
||||
|
|
|
@ -1288,4 +1288,4 @@
|
|||
"svg": null,
|
||||
"color": ""
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -1 +1 @@
|
|||
"29065815"
|
||||
"29067102"
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
* {
|
||||
font-family: Courier New,
|
||||
Courier,
|
||||
Lucida Sans Typewriter,
|
||||
Lucida Typewriter,
|
||||
monospace;
|
||||
font-family:
|
||||
Courier New,
|
||||
Courier,
|
||||
Lucida Sans Typewriter,
|
||||
Lucida Typewriter,
|
||||
monospace;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
@ -469,27 +470,45 @@ h2.histogram-title strong {
|
|||
max-width: none;
|
||||
|
||||
table {
|
||||
th:hover{
|
||||
th:hover {
|
||||
cursor: pointer;
|
||||
background: black;
|
||||
}
|
||||
td, th {
|
||||
td,
|
||||
th {
|
||||
padding: 0 5px;
|
||||
line-height: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th:first-child, td:first-child {
|
||||
text-align: left
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
img{
|
||||
img {
|
||||
width: 20px;
|
||||
height: auto;
|
||||
height: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
tr:nth-child(2n) {
|
||||
background: rgba(0, 0, 0, 0.58);;
|
||||
}
|
||||
tr:nth-child(2n) {
|
||||
background: rgba(0, 0, 0, 0.58);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-inline {
|
||||
position: absolute;
|
||||
display: block;
|
||||
background: grey;
|
||||
left: 62px;
|
||||
right: 2px;
|
||||
height: 7px;
|
||||
bottom: 2px;
|
||||
border-radius: 2px;
|
||||
span {
|
||||
position: absolute;
|
||||
inset: 1px;
|
||||
transform-origin: top left;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
|
108
src/game.ts
108
src/game.ts
|
@ -1,6 +1,16 @@
|
|||
import {allLevels, appVersion, icons, upgrades} from "./loadGameData";
|
||||
import {Ball, Coin, GameState, LightFlash, OptionId, ParticleFlash, PerkId, RunParams, TextFlash,} from "./types";
|
||||
import {getAudioContext, playPendingSounds} from "./sounds";
|
||||
import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
|
||||
import {
|
||||
Ball,
|
||||
Coin,
|
||||
GameState,
|
||||
LightFlash,
|
||||
OptionId,
|
||||
ParticleFlash,
|
||||
PerkId,
|
||||
RunParams,
|
||||
TextFlash,
|
||||
} from "./types";
|
||||
import { getAudioContext, playPendingSounds } from "./sounds";
|
||||
import {
|
||||
currentLevelInfo,
|
||||
describeLevel,
|
||||
|
@ -13,7 +23,7 @@ import {
|
|||
} from "./game_utils";
|
||||
|
||||
import "./PWA/sw_loader";
|
||||
import {getCurrentLang, t} from "./i18n/i18n";
|
||||
import { getCurrentLang, t } from "./i18n/i18n";
|
||||
import {
|
||||
cycleMaxCoins,
|
||||
cycleMaxParticles,
|
||||
|
@ -30,21 +40,40 @@ import {
|
|||
setLevel,
|
||||
setMousePos,
|
||||
} from "./gameStateMutators";
|
||||
import {backgroundCanvas, gameCanvas, haloCanvas, haloScale, render, scoreDisplay,} from "./render";
|
||||
import {pauseRecording, recordOneFrame, resumeRecording, startRecordingGame,} from "./recording";
|
||||
import {newGameState} from "./newGameState";
|
||||
import {alertsOpen, asyncAlert, AsyncAlertAction, closeModal, requiredAsyncAlert,} from "./asyncAlert";
|
||||
import {isOptionOn, options, toggleOption} from "./options";
|
||||
import {hashCode} from "./getLevelBackground";
|
||||
import {hoursSpentPlaying} from "./pure_functions";
|
||||
import {helpMenuEntry} from "./help";
|
||||
import {creativeMode} from "./creative";
|
||||
import {setupTooltips} from "./tooltip";
|
||||
import {startingPerkMenuButton} from "./startingPerks";
|
||||
import {
|
||||
backgroundCanvas,
|
||||
gameCanvas,
|
||||
haloCanvas,
|
||||
haloScale,
|
||||
render,
|
||||
scoreDisplay,
|
||||
} from "./render";
|
||||
import {
|
||||
pauseRecording,
|
||||
recordOneFrame,
|
||||
resumeRecording,
|
||||
startRecordingGame,
|
||||
} from "./recording";
|
||||
import { newGameState } from "./newGameState";
|
||||
import {
|
||||
alertsOpen,
|
||||
asyncAlert,
|
||||
AsyncAlertAction,
|
||||
closeModal,
|
||||
requiredAsyncAlert,
|
||||
} from "./asyncAlert";
|
||||
import { isOptionOn, options, toggleOption } from "./options";
|
||||
import { hashCode } from "./getLevelBackground";
|
||||
import { hoursSpentPlaying } from "./pure_functions";
|
||||
import { helpMenuEntry } from "./help";
|
||||
import { creativeMode } from "./creative";
|
||||
import { setupTooltips } from "./tooltip";
|
||||
import { startingPerkMenuButton } from "./startingPerks";
|
||||
import "./migrations";
|
||||
import {getCreativeModeWarning, getHistory} from "./gameOver";
|
||||
import {generateSaveFileContent} from "./generateSaveFileContent";
|
||||
import {runHistoryViewerMenuEntry} from "./runHistoryViewer";
|
||||
import { getHistory } from "./gameOver";
|
||||
import { generateSaveFileContent } from "./generateSaveFileContent";
|
||||
import { runHistoryViewerMenuEntry } from "./runHistoryViewer";
|
||||
import { getNearestUnlockHTML, openScorePanel } from "./openScorePanel";
|
||||
|
||||
export async function play() {
|
||||
if (await applyFullScreenChoice()) return;
|
||||
|
@ -267,6 +296,8 @@ export async function openUpgradesPicker(gameState: GameState) {
|
|||
<p>${levelsListHTMl(gameState, gameState.currentLevel + 1)}</p>
|
||||
`,
|
||||
...actions,
|
||||
|
||||
getNearestUnlockHTML(gameState),
|
||||
pickedUpgradesHTMl(gameState),
|
||||
|
||||
`<div id="level-recording-container"></div>`,
|
||||
|
@ -400,7 +431,7 @@ window.addEventListener("visibilitychange", () => {
|
|||
scoreDisplay.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
if (!alertsOpen) {
|
||||
openScorePanel();
|
||||
openScorePanel(gameState);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -410,28 +441,6 @@ document.addEventListener("visibilitychange", () => {
|
|||
}
|
||||
});
|
||||
|
||||
async function openScorePanel() {
|
||||
pause(true);
|
||||
|
||||
await asyncAlert({
|
||||
title: t("score_panel.title", {
|
||||
score: gameState.score,
|
||||
level: gameState.currentLevel + 1,
|
||||
max: max_levels(gameState),
|
||||
}),
|
||||
|
||||
content: [
|
||||
getCreativeModeWarning(gameState),
|
||||
pickedUpgradesHTMl(gameState),
|
||||
levelsListHTMl(gameState, gameState.currentLevel),
|
||||
gameState.rerolls
|
||||
? t("score_panel.rerolls_count", { rerolls: gameState.rerolls })
|
||||
: "",
|
||||
],
|
||||
allowClose: true,
|
||||
});
|
||||
}
|
||||
|
||||
(document.getElementById("menu") as HTMLButtonElement).addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
|
@ -461,7 +470,7 @@ export async function openMainMenu() {
|
|||
},
|
||||
},
|
||||
creativeMode(gameState),
|
||||
runHistoryViewerMenuEntry(),
|
||||
runHistoryViewerMenuEntry(),
|
||||
{
|
||||
icon: icons["icon:unlocks"],
|
||||
text: t("main_menu.unlocks"),
|
||||
|
@ -568,8 +577,7 @@ async function openSettingsMenu() {
|
|||
text: t("main_menu.download_save_file"),
|
||||
help: t("main_menu.download_save_file_help"),
|
||||
async value() {
|
||||
|
||||
const signedPayload =generateSaveFileContent()
|
||||
const signedPayload = generateSaveFileContent();
|
||||
|
||||
const dlLink = document.createElement("a");
|
||||
|
||||
|
@ -796,13 +804,17 @@ async function openUnlocksList() {
|
|||
}));
|
||||
|
||||
const levelActions = allLevels.map((l, li) => {
|
||||
const problem = reasonLevelIsLocked(li, getHistory());
|
||||
const lockedBecause = reasonLevelIsLocked(li, getHistory(), true);
|
||||
const percentUnlocked = lockedBecause?.reached
|
||||
? `<span class="progress-inline"><span style="transform: scale(${Math.floor((lockedBecause.reached / lockedBecause.minScore) * 100) / 100},1)"></span></span>`
|
||||
: "";
|
||||
|
||||
return {
|
||||
text: l.name,
|
||||
disabled: !!problem,
|
||||
text: l.name + percentUnlocked,
|
||||
disabled: !!lockedBecause,
|
||||
value: { level: l.name } as RunParams,
|
||||
icon: icons[l.name],
|
||||
[hintField]: problem || describeLevel(l),
|
||||
[hintField]: lockedBecause?.text || describeLevel(l),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -125,16 +125,17 @@ export function getHistograms(gameState: GameState) {
|
|||
.map((l, li) => ({
|
||||
li,
|
||||
l,
|
||||
r: reasonLevelIsLocked(li, runsHistory),
|
||||
r: reasonLevelIsLocked(li, runsHistory, false)?.text,
|
||||
}))
|
||||
.filter((l) => l.r);
|
||||
|
||||
|
||||
gameState.runStatistics.runTime=Math.round(gameState.runStatistics.runTime)
|
||||
const perks={...gameState.perks}
|
||||
for(let id in perks){
|
||||
if(!perks[id]){
|
||||
delete perks[id]
|
||||
gameState.runStatistics.runTime = Math.round(
|
||||
gameState.runStatistics.runTime,
|
||||
);
|
||||
const perks = { ...gameState.perks };
|
||||
for (let id in perks) {
|
||||
if (!perks[id]) {
|
||||
delete perks[id];
|
||||
}
|
||||
}
|
||||
runsHistory.push({
|
||||
|
@ -144,7 +145,7 @@ export function getHistograms(gameState: GameState) {
|
|||
});
|
||||
|
||||
const unlocked = locked.filter(
|
||||
({ li }) => !reasonLevelIsLocked(li, runsHistory),
|
||||
({ li }) => !reasonLevelIsLocked(li, runsHistory, true),
|
||||
);
|
||||
if (unlocked.length) {
|
||||
unlockedLevels = `
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
PerkId,
|
||||
PerksMap,
|
||||
RunHistoryItem,
|
||||
Upgrade,
|
||||
} from "./types";
|
||||
import { icons, upgrades } from "./loadGameData";
|
||||
import { t } from "./i18n/i18n";
|
||||
|
@ -278,51 +279,96 @@ export function highScoreText() {
|
|||
return "";
|
||||
}
|
||||
|
||||
type UpgradeLike = { id: PerkId; name: string; requires: string };
|
||||
|
||||
export function getLevelUnlockCondition(levelIndex: number) {
|
||||
// Returns "" if level is unlocked, otherwise a string explaining how to unlock it
|
||||
let required: UpgradeLike[] = [],
|
||||
forbidden: UpgradeLike[] = [],
|
||||
minScore = 0;
|
||||
if (levelIndex <= 10) {
|
||||
// Keep all as is
|
||||
} else if (levelIndex < 20) {
|
||||
minScore = 100 * levelIndex;
|
||||
} else {
|
||||
const excluded: Set<PerkId> = new Set([
|
||||
"extra_levels",
|
||||
"extra_life",
|
||||
"one_more_choice",
|
||||
"instant_upgrade",
|
||||
"shunt",
|
||||
"slow_down",
|
||||
]);
|
||||
// Avoid excluding a perk that's needed for the required one
|
||||
rawUpgrades.forEach((u) => {
|
||||
if (u.requires) excluded.add(u.requires);
|
||||
});
|
||||
|
||||
const possibletargets = rawUpgrades
|
||||
.slice(0, Math.floor(levelIndex / 2))
|
||||
.map((u) => u)
|
||||
.filter((u) => !excluded.has(u.id))
|
||||
.sort(
|
||||
(a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id),
|
||||
);
|
||||
|
||||
const length = Math.ceil(levelIndex / 30);
|
||||
required = possibletargets.slice(0, length);
|
||||
forbidden = possibletargets.slice(length, length + length);
|
||||
minScore = 100 * levelIndex;
|
||||
}
|
||||
return {
|
||||
required,
|
||||
forbidden,
|
||||
minScore,
|
||||
};
|
||||
}
|
||||
|
||||
export function getBestScoreMatching(
|
||||
history: RunHistoryItem[],
|
||||
required: UpgradeLike[] = [],
|
||||
forbidden: UpgradeLike[] = [],
|
||||
) {
|
||||
return Math.max(
|
||||
0,
|
||||
...history
|
||||
.filter(
|
||||
(r) =>
|
||||
!required.find((u) => !r?.perks?.[u.id]) &&
|
||||
!forbidden.find((u) => r?.perks?.[u.id]),
|
||||
)
|
||||
.map((r) => r.score),
|
||||
);
|
||||
}
|
||||
|
||||
export function reasonLevelIsLocked(
|
||||
levelIndex: number,
|
||||
history: RunHistoryItem[],
|
||||
) {
|
||||
// Returns "" if level is unlocked, otherwise a string explaining how to unlock it
|
||||
if (levelIndex <= 10) {
|
||||
return "";
|
||||
}
|
||||
if (levelIndex < 20) {
|
||||
const minScore = 100 * levelIndex;
|
||||
return history.find((r) => r.score >= minScore)
|
||||
? ""
|
||||
: t("unlocks.minScore", { minScore });
|
||||
}
|
||||
const excluded: PerkId[] = [
|
||||
"extra_levels",
|
||||
"extra_life",
|
||||
"one_more_choice",
|
||||
"instant_upgrade",
|
||||
];
|
||||
mentionBestScore: boolean,
|
||||
): null | { reached: number; minScore: number; text: string } {
|
||||
const { required, forbidden, minScore } = getLevelUnlockCondition(levelIndex);
|
||||
|
||||
const possibletargets = rawUpgrades
|
||||
.slice(0, Math.floor(levelIndex / 2))
|
||||
.map((u) => u)
|
||||
.filter((u) => !excluded.includes(u.id))
|
||||
.sort((a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id));
|
||||
|
||||
const length = Math.ceil(levelIndex / 30);
|
||||
const required = possibletargets.slice(0, length);
|
||||
const forbidden = possibletargets.slice(length, length + length);
|
||||
const minScore = 100 * levelIndex * Math.floor(Math.pow(1.01, levelIndex));
|
||||
if (
|
||||
history.find(
|
||||
(r) =>
|
||||
r.score >= minScore &&
|
||||
!required.find((u) => !r?.perks?.[u.id]) &&
|
||||
!forbidden.find((u) => r?.perks?.[u.id]),
|
||||
)
|
||||
) {
|
||||
return "";
|
||||
} else {
|
||||
return t("unlocks.minScoreWithPerks", {
|
||||
const reached = getBestScoreMatching(history, required, forbidden);
|
||||
let reachedText =
|
||||
reached && mentionBestScore ? t("unlocks.reached", { reached }) : "";
|
||||
if (reached >= minScore) {
|
||||
return null;
|
||||
} else if (!required.length && !forbidden.length) {
|
||||
return {
|
||||
reached,
|
||||
minScore,
|
||||
required: required.map((u) => u.name).join(", "),
|
||||
forbidden: forbidden.map((u) => u.name).join(", "),
|
||||
});
|
||||
text: t("unlocks.minScore", { minScore }) + reachedText,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
reached,
|
||||
minScore,
|
||||
text:
|
||||
t("unlocks.minScoreWithPerks", {
|
||||
minScore,
|
||||
required: required.map((u) => u.name).join(", "),
|
||||
forbidden: forbidden.map((u) => u.name).join(", "),
|
||||
}) + reachedText,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
export function generateSaveFileContent() {
|
||||
const localStorageContent: Record<string, string> = {};
|
||||
|
||||
const localStorageContent: Record<string, string> = {};
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i) as string;
|
||||
// Avoid including recovery info in the recovery info
|
||||
if(['recovery_data'].includes(key)) continue
|
||||
const value = localStorage.getItem(key) as string;
|
||||
localStorageContent[key] = value;
|
||||
}
|
||||
return JSON.stringify(localStorageContent);
|
||||
}
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i) as string;
|
||||
// Avoid including recovery info in the recovery info
|
||||
if (["recovery_data"].includes(key)) continue;
|
||||
const value = localStorage.getItem(key) as string;
|
||||
localStorageContent[key] = value;
|
||||
}
|
||||
return JSON.stringify(localStorageContent);
|
||||
}
|
||||
|
|
|
@ -2247,6 +2247,51 @@
|
|||
<folder_node>
|
||||
<name>score_panel</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>close_to_unlock</name>
|
||||
<description/>
|
||||
<comment/>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-FR</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>continue_to_unlock</name>
|
||||
<description/>
|
||||
<comment/>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-FR</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>get_upgrades_to_unlock</name>
|
||||
<description/>
|
||||
<comment/>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-FR</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>rerolls_count</name>
|
||||
<description/>
|
||||
|
@ -2262,6 +2307,21 @@
|
|||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>score_to_unlock</name>
|
||||
<description/>
|
||||
<comment/>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-FR</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>title</name>
|
||||
<description/>
|
||||
|
@ -2462,6 +2522,21 @@
|
|||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>reached</name>
|
||||
<description/>
|
||||
<comment/>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-FR</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>title_upgrades</name>
|
||||
<description/>
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
"main_menu.contrast": "High Contrast",
|
||||
"main_menu.contrast_help": "More colorful and dark rendering",
|
||||
"main_menu.credit_levels": "<h2>Levels source or reference link</h2>",
|
||||
"main_menu.credits": "# Credits\n\nI pulled many background patterns from https://pattern.monster/\n\nSome of the sound generating code was written by ChatGPT, and heavily\nadapted to my usage over time.\n\nI wanted an APK to start in fullscreen and be able to list it on fdroid and the play store. I started with an empty view and went to work trimming it down, with the help of that tutorial : https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md\n\nColin (obigre) brought a lot of fantastic ideas to the game, here's his website (in French) : https://colin-crapahute.bearblog.dev/\n\n# Breakout games suggestions\n\nHere are a few interesting games in the breakout genre :\n\n- LBreakoutHD : https://sourceforge.net/p/lgames/code/HEAD/tree/trunk/lbreakouthd/\n- Wizorb : https://store.steampowered.com/app/207420/Wizorb/\n- Ricochet infinity : https://www.myabandonware.com/game/ricochet-infinity-dxm\n- First prototype of B71 : https://breakout-v1.lecaro.me/\n- Second prototype of B71: https://breakout-v2.lecaro.me/\n\n\n# PC game suggestions\n\nHere are a few games i've sank a lot of time in, and that inspired breakout in some way\n\n- Heat signature : https://www.humblebundle.com/store/heat-signature\n- FTL : https://www.gog.com/en/game/faster_than_light\n- Nova drift : https://www.gog.com/en/game/nova_drift\n- Noita : https://www.gog.com/en/game/noita\n- Enter the gungeon : https://www.gog.com/en/game/enter_the_gungeon\n- Zero Sivert : https://store.steampowered.com/app/1782120/ZERO_Sievert/\n- Factorio : https://www.factorio.com/\n- Nuclear throne : https://store.steampowered.com/app/242680/Nuclear_Throne/ (don't buy on GOG it's outdated) \n- Brigador : https://www.gog.com/en/game/brigador\n- Teleglitch https://www.gog.com/en/game/teleglitch_die_more_edition\n- Rollers of the realm : https://store.steampowered.com/app/262470/Rollers_of_the_Realm/\n",
|
||||
"main_menu.credits": "# Credits\n\nI pulled many background patterns from https://pattern.monster/\n\nSome of the sound generating code was written by ChatGPT, and heavily\nadapted to my usage over time.\n\nI wanted an APK to start in fullscreen and be able to list it on fdroid and the play store. I started with an empty view and went to work trimming it down, with the help of that tutorial : https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md\n\nColin (obigre) brought a lot of fantastic ideas to the game, here's his website (in French) : https://colin-crapahute.bearblog.dev/\n\nTõnu Rääk made a Tiermaker template to share your favorite perk choices : https://tiermaker.com/create/breakout-71-perks-18086724\n\n# Breakout games suggestions\n\nHere are a few interesting games in the breakout genre :\n\n- LBreakoutHD : https://sourceforge.net/p/lgames/code/HEAD/tree/trunk/lbreakouthd/\n- Wizorb : https://store.steampowered.com/app/207420/Wizorb/\n- Ricochet infinity : https://www.myabandonware.com/game/ricochet-infinity-dxm\n- First prototype of B71 : https://breakout-v1.lecaro.me/\n- Second prototype of B71: https://breakout-v2.lecaro.me/\n- Whackerball : https://store.steampowered.com/app/2192170/Whackerball/\n\n# PC game suggestions\n\nHere are a few games i've sank a lot of time in, and that inspired breakout in some way\n\n- Heat signature : https://www.humblebundle.com/store/heat-signature\n- FTL : https://www.gog.com/en/game/faster_than_light\n- Nova drift : https://www.gog.com/en/game/nova_drift\n- Noita : https://www.gog.com/en/game/noita\n- Enter the gungeon : https://www.gog.com/en/game/enter_the_gungeon\n- Zero Sivert : https://store.steampowered.com/app/1782120/ZERO_Sievert/\n- Factorio : https://www.factorio.com/\n- Nuclear throne : https://store.steampowered.com/app/242680/Nuclear_Throne/ (don't buy on GOG it's outdated) \n- Brigador : https://www.gog.com/en/game/brigador\n- Teleglitch https://www.gog.com/en/game/teleglitch_die_more_edition\n- Rollers of the realm : https://store.steampowered.com/app/262470/Rollers_of_the_Realm/\n",
|
||||
"main_menu.donate": "You've played for {{hours}} hours",
|
||||
"main_menu.donate_help": "How about donating {{suggestion}} € ? You can hide this reminder in the settings. ",
|
||||
"main_menu.donation_reminder": "Remind me to donate",
|
||||
|
@ -143,7 +143,11 @@
|
|||
"play.stats.levelMisses": "Missed shots, where you hit nothing",
|
||||
"play.stats.levelTime": "Level time",
|
||||
"play.stats.levelWallBounces": "Wall bounces",
|
||||
"score_panel.close_to_unlock": "You could unlock a level at the end of this run:",
|
||||
"score_panel.continue_to_unlock": "You are about to unlock level \"{{level}}\"",
|
||||
"score_panel.get_upgrades_to_unlock": "Get {{missingUpgrades}} and score {{points}} more points to unlock level \"{{level}}\"",
|
||||
"score_panel.rerolls_count": "You have accumulated {{rerolls}} rerolls",
|
||||
"score_panel.score_to_unlock": "Score {{points}} more points to unlock level \"{{level}}\"",
|
||||
"score_panel.title": "{{score}} points at level {{level}}/{{max}} ",
|
||||
"score_panel.title_looped": "{{score}} points at level {{level}}/{{max}} of loop {{loop}}",
|
||||
"score_panel.upcoming_levels": "Upcoming levels :",
|
||||
|
@ -154,9 +158,10 @@
|
|||
"unlocks.just_unlocked_plural": "You just unlocked {{count}} levels",
|
||||
"unlocks.level": "<h2>You unlocked {{unlocked}} levels out of {{out_of}}</h2>\n<p>Here are all the game levels, click one to start a game with that starting level. </p> ",
|
||||
"unlocks.level_description": "A {{size}}x{{size}} level with {{bricks}} bricks, {{colors}} colors and {{bombs}} bombs.",
|
||||
"unlocks.minScore": "Reach ${{minScore}}",
|
||||
"unlocks.minScoreWithPerks": "Reach ${{minScore}} in a run with {{required}} but without {{forbidden}}",
|
||||
"unlocks.minScore": "Reach ${{minScore}} in a run to unlock.",
|
||||
"unlocks.minScoreWithPerks": "Reach ${{minScore}} in a run with {{required}} but without {{forbidden}} to unlock.",
|
||||
"unlocks.minTotalScore": "Accumulate a total of ${{score}}",
|
||||
"unlocks.reached": "Your best score was {{reached}}.",
|
||||
"unlocks.title_upgrades": "You unlocked {{unlocked}} upgrades out of {{out_of}}",
|
||||
"upgrades.addiction.fullHelp": "The countdown only starts after breaking the first brick of each level. It stops as soon as all bricks are destroyed.",
|
||||
"upgrades.addiction.help": "+{{lvl}} combo / brick, combo resets {{delay}}s after breaking a brick. ",
|
||||
|
|
|
@ -24,18 +24,18 @@
|
|||
"gameOver.unlocked_perk_plural": "Vous avez débloqué {{count}} améliorations",
|
||||
"gameOver.win.summary": "Cette partie est terminée. Vous avez accumulé {{score}} pièces. ",
|
||||
"gameOver.win.title": "Vous avez terminé cette partie",
|
||||
"history.columns.max_combo": "",
|
||||
"history.columns.max_level": "",
|
||||
"history.columns.puck_bounces": "",
|
||||
"history.columns.puck_bounces_tooltip": "",
|
||||
"history.columns.max_combo": "Combo maximum",
|
||||
"history.columns.max_level": "Les niveaux",
|
||||
"history.columns.puck_bounces": "PB",
|
||||
"history.columns.puck_bounces_tooltip": "Rebonds du palet : nombre de fois où la balle a rebondi sur le palet",
|
||||
"history.columns.runTime": "Dur.",
|
||||
"history.columns.runTime_tooltip": "",
|
||||
"history.columns.score": "",
|
||||
"history.columns.started": "",
|
||||
"history.columns.upgrades_picked": "",
|
||||
"history.help": "",
|
||||
"history.locked": "",
|
||||
"history.title": "",
|
||||
"history.columns.runTime_tooltip": "Durée de la partie, en secondes, en comptant uniquement le temps où le jeu se déroule et où la balle est en mouvement",
|
||||
"history.columns.score": "Score",
|
||||
"history.columns.started": "Date",
|
||||
"history.columns.upgrades_picked": "Mises à niveau",
|
||||
"history.help": "Voir la liste de votre jeu {{count}} ",
|
||||
"history.locked": "Jouez d'abord au moins dix parties",
|
||||
"history.title": "Historique",
|
||||
"lab.help": "Essayez n'importe quel build",
|
||||
"lab.instructions": "Sélectionnez les améliorations ci-dessous, puis choisissez le niveau à jouer. Les parties en mode créatif sont ignorées dans les déblocages, le meilleur score, le score total et les statistiques, et ne durent qu'un seul niveau.",
|
||||
"lab.menu_entry": "Mode créatif",
|
||||
|
@ -68,7 +68,7 @@
|
|||
"main_menu.contrast": "Contraste élevé",
|
||||
"main_menu.contrast_help": "Affichage plus contrasté et coloré",
|
||||
"main_menu.credit_levels": "<h2>Source ou référence des niveaux</h2>",
|
||||
"main_menu.credits": "# Crédits\n\nJ'ai récupéré de nombreux motifs d'arrière-plan sur https://pattern.monster/\n\nUne partie du code de génération de sons a été écrite par ChatGPT et a été largement adaptée à mon utilisation au fil du temps.\n\nJe souhaitais un APK qui démarre en plein écran et puisse être listé sur Android et le Play Store. J'ai commencé avec une vue vide et je me suis attelé à la réduire, à l'aide de ce tutoriel : https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md\n\nColin (obigre) a apporté de nombreuses idées fantastiques au jeu. Voici son site web : https://colin-crapahute.bearblog.dev/\n\n# Autres jeux de casse-briques\n\nVoici quelques jeux intéressants dans le genre du casse-briques :\n\n- LBreakoutHD : un remake open source intéressant https://sourceforge.net/p/lgames/code/HEAD/tree/trunk/lbreakouthd/\n- Wizorb https://store.steampowered.com/app/207420/Wizorb/\n- Breakout multijoueur : JcJ avec multijoueur de type console aérienne https://casmo.itch.io/breakout-multiplayer\n- Ricochet Infinity : https://www.myabandonware.com/game/ricochet-infinity-dxm\n- Mes premières tentatives dans le genre : https://breakout-v1.lecaro.me/ (décontracté, plus proche du concept original de Breakout) et https://breakout-v2.lecaro.me/ (multijoueur)\n\n# Autres jeux PC à forte rejouabilité\n\n- Rollers of the realm : https://store.steampowered.com/app/262470/Rollers_of_the_Realm/\n- FTL : https://www.gog.com/en/game/faster_than_light\n- Nova Drift : https://www.gog.com/en/game/nova_drift\n- Noita : https://www.gog.com/en/game/noita\n- Enter the Gungeon : https://www.gog.com/en/game/enter_the_gungeon\n- Zero Sivert : https://store.steampowered.com/app/1782120/ZERO_Sievert/\n- Factorio : https://www.factorio.com/\n- Nuclear throne : https://store.steampowered.com/app/242680/Nuclear_Throne/ (ne l'achetez pas sur GOG, c'est obsolète)\n- Brigador : https://www.gog.com/en/game/brigador\n- Teleglitch https://www.gog.com/en/game/teleglitch_die_more_edition\n- Broforce : https://www.gog.com/en/game/broforce\n- Spelunky : https://www.gog.com/en/game/spelunky",
|
||||
"main_menu.credits": "# Crédits\n\nJ'ai récupéré de nombreux motifs d'arrière-plan sur https://pattern.monster/\n\nUne partie du code de génération de sons a été écrite par ChatGPT et a été largement adaptée à mon utilisation au fil du temps.\n\nJe souhaitais un APK qui démarre en plein écran et puisse être listé sur Android et le Play Store. J'ai commencé avec une vue vide et je me suis attelé à la réduire, à l'aide de ce tutoriel : https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md\n\nColin (obigre) a apporté de nombreuses idées fantastiques au jeu. Voici son site web : https://colin-crapahute.bearblog.dev/\n\nTõnu Rääk a fait un template Tiermaker pour partager vos améliorations préférées : https://tiermaker.com/create/breakout-71-perks-18086724\n\n# Autres jeux de casse-briques\n\nVoici quelques jeux intéressants dans le genre du casse-briques :\n\n- LBreakoutHD : un remake open source intéressant https://sourceforge.net/p/lgames/code/HEAD/tree/trunk/lbreakouthd/\n- Wizorb https://store.steampowered.com/app/207420/Wizorb/\n- Breakout multijoueur : JcJ avec multijoueur de type console aérienne https://casmo.itch.io/breakout-multiplayer\n- Ricochet Infinity : https://www.myabandonware.com/game/ricochet-infinity-dxm\n- Mes premières tentatives dans le genre : https://breakout-v1.lecaro.me/ (décontracté, plus proche du concept original de Breakout) et https://breakout-v2.lecaro.me/ (multijoueur)\n\n# Autres jeux PC à forte rejouabilité\n\n- Rollers of the realm : https://store.steampowered.com/app/262470/Rollers_of_the_Realm/\n- FTL : https://www.gog.com/en/game/faster_than_light\n- Nova Drift : https://www.gog.com/en/game/nova_drift\n- Noita : https://www.gog.com/en/game/noita\n- Enter the Gungeon : https://www.gog.com/en/game/enter_the_gungeon\n- Zero Sivert : https://store.steampowered.com/app/1782120/ZERO_Sievert/\n- Factorio : https://www.factorio.com/\n- Nuclear throne : https://store.steampowered.com/app/242680/Nuclear_Throne/ (ne l'achetez pas sur GOG, c'est obsolète)\n- Brigador : https://www.gog.com/en/game/brigador\n- Teleglitch https://www.gog.com/en/game/teleglitch_die_more_edition\n- Broforce : https://www.gog.com/en/game/broforce\n- Spelunky : https://www.gog.com/en/game/spelunky",
|
||||
"main_menu.donate": "Vous avez joué {{hours}} heures",
|
||||
"main_menu.donate_help": "Pourriez-vous donner {{suggestion}} € ? Vous pouvez masquer ce rappel dans les paramètres.",
|
||||
"main_menu.donation_reminder": "Me rappeler de donner",
|
||||
|
@ -143,7 +143,11 @@
|
|||
"play.stats.levelMisses": "Tirs ratés, ou vous n'avez touché aucune brique",
|
||||
"play.stats.levelTime": "Durée du niveau",
|
||||
"play.stats.levelWallBounces": "Rebonds sur les murs",
|
||||
"score_panel.close_to_unlock": "Vous pourriez débloquer un niveau à la fin de cette partie :",
|
||||
"score_panel.continue_to_unlock": "Vous êtes sur le point de débloquer le niveau « {{level}} »",
|
||||
"score_panel.get_upgrades_to_unlock": "Obtenez {{missingUpgrades}} et marquez {{points}} points supplémentaires pour débloquer le niveau « {{level}} »",
|
||||
"score_panel.rerolls_count": "Vous avez accumulé {{rerolls}} rerolls",
|
||||
"score_panel.score_to_unlock": "Marquez {{points}} points supplémentaires pour débloquer le niveau « {{level}} »",
|
||||
"score_panel.title": "{{score}} points au niveau {{level}}/{{max}} ",
|
||||
"score_panel.title_looped": "{{score}} points au niveau {{level}}/{{max}} ",
|
||||
"score_panel.upcoming_levels": "Niveaux de la parties : ",
|
||||
|
@ -154,9 +158,10 @@
|
|||
"unlocks.just_unlocked_plural": "Vous venez de débloquer {{count}} niveaux",
|
||||
"unlocks.level": "<h2>Vous avez débloqué {{unlocked}} niveaux sur {{out_of}}</h2>\n<p>Voici tous les niveaux du jeu, cliquez sur l'un d'eux pour démarrer une partie avec ce niveau de départ. </p> ",
|
||||
"unlocks.level_description": "Un niveau {{size}}x{{size}} avec {{bricks}} briques, {{colors}} couleurs et {{bombs}} bombes.",
|
||||
"unlocks.minScore": "Atteindre ${{minScore}}",
|
||||
"unlocks.minScoreWithPerks": "Atteignez ${{minScore}} dans une course avec {{required}} mais sans {{forbidden}}",
|
||||
"unlocks.minScore": "Atteignez un score de ${{minScore}} dans une partie pour débloquer.",
|
||||
"unlocks.minScoreWithPerks": "Atteignez ${{minScore}} dans une partie avec {{required}} mais sans {{forbidden}}.",
|
||||
"unlocks.minTotalScore": "Accumuler un total de ${{score}}",
|
||||
"unlocks.reached": "Votre meilleur score pour l'instant est {{reached}}.",
|
||||
"unlocks.title_upgrades": "Vous avez débloqué {{unlocked}} améliorations sur {{out_of}}",
|
||||
"upgrades.addiction.fullHelp": "Le décompte ne commence qu'à parti de la destruction de la première brique du niveau, et s'arrête dès qu'il n'y a plus de briques. ",
|
||||
"upgrades.addiction.help": "+{{lvl}} combo / brique, le combo RAZ après {{delay}}s sans casser de briques",
|
||||
|
@ -229,7 +234,7 @@
|
|||
"upgrades.implosions.help": "Les explosions aspirent les pièces au lieu de les faire exploser.",
|
||||
"upgrades.implosions.name": "Implosions",
|
||||
"upgrades.instant_upgrade.fullHelp": "Choisissez immédiatement deux améliorations, afin d'en obtenir une gratuite et une autre pour rembourser celle utilisée pour obtenir cet avantage. Chaque fois que vous choisirez des améliorations dans le menu suivant, vous aurez moins de choix.",
|
||||
"upgrades.instant_upgrade.help": "-{{lvl}} choix jusqu'à la fin de la course.",
|
||||
"upgrades.instant_upgrade.help": "-{{lvl}} choix jusqu'à la fin de la partie.",
|
||||
"upgrades.instant_upgrade.name": "+2 améliorations maintenant",
|
||||
"upgrades.left_is_lava.fullHelp": "Chaque fois que vous cassez une brique, votre combo augmente d'une unité, ce qui vous permet d'obtenir une pièce de plus à chaque fois que vous cassez une brique.\n\nCependant, votre combinaison se réinitialise dès que votre balle touche le côté gauche.\n\nDès que votre combo augmente, le côté gauche devient rouge pour vous rappeler que vous devez éviter de le frapper.",
|
||||
"upgrades.left_is_lava.help": "+{{lvl}} combo par brique, RAZ en touchant le bord gauche",
|
||||
|
@ -295,7 +300,7 @@
|
|||
"upgrades.skip_last.help": "La dernière brique s'autodétruit.",
|
||||
"upgrades.skip_last.help_plural": "Les {{lvl}} dernières briques restantes s'autodétruiront",
|
||||
"upgrades.skip_last.name": "Nettoyage facile",
|
||||
"upgrades.slow_down.fullHelp": "La balle démarre relativement lentement, mais à chaque niveau de votre course, elle démarre un peu plus vite, et elle accélère également si vous passez beaucoup de temps dans un niveau.\n\nCet avantage rend la balle plus facile à gérer. \n\nVous pouvez l'obtenir au début de chaque course en activant le mode enfant dans le menu.",
|
||||
"upgrades.slow_down.fullHelp": "La balle démarre relativement lentement, mais à chaque niveau de votre partie, elle démarre un peu plus vite, et elle accélère également si vous passez beaucoup de temps dans un niveau.\n\nCet avantage rend la balle plus facile à gérer. \n\nVous pouvez l'obtenir au début de chaque partie en activant le mode enfant dans le menu.",
|
||||
"upgrades.slow_down.help": "La balle se déplace plus lentement",
|
||||
"upgrades.slow_down.name": "Balle lente",
|
||||
"upgrades.smaller_puck.fullHelp": "Le palet est donc plus petit, ce qui, en théorie, facilite certains tirs en coin, mais augmente surtout la difficulté.\n\nC'est pourquoi vous bénéficiez également d'un bonus de +5 pièces par brique pour toutes les briques que vous casserez après avoir choisi cette option.",
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
import { RunHistoryItem } from "./types";
|
||||
|
||||
import _appVersion from "./data/version.json";
|
||||
import {generateSaveFileContent} from "./generateSaveFileContent";
|
||||
import { generateSaveFileContent } from "./generateSaveFileContent";
|
||||
|
||||
// The page will be reloaded if any migrations were run
|
||||
let migrationsRun=0
|
||||
let migrationsRun = 0;
|
||||
function migrate(name: string, cb: () => void) {
|
||||
if (!localStorage.getItem(name)) {
|
||||
try {
|
||||
cb();
|
||||
console.debug("Ran migration : " + name);
|
||||
localStorage.setItem(name, "" + Date.now());
|
||||
migrationsRun++
|
||||
migrationsRun++;
|
||||
} catch (e) {
|
||||
console.warn("Migration " + name + " failed : ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
migrate("save_data_before_upgrade_to_"+_appVersion, () => {
|
||||
localStorage.setItem("recovery_data",JSON.stringify(generateSaveFileContent()));
|
||||
migrate("save_data_before_upgrade_to_" + _appVersion, () => {
|
||||
localStorage.setItem(
|
||||
"recovery_data",
|
||||
JSON.stringify(generateSaveFileContent()),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
migrate("migrate_high_scores", () => {
|
||||
const old = localStorage.getItem("breakout-3-hs");
|
||||
if (old) {
|
||||
|
@ -53,7 +55,7 @@ migrate("remove_long_and_creative_mode_data", () => {
|
|||
) as RunHistoryItem[];
|
||||
|
||||
let cleaned = runsHistory.filter((r) => {
|
||||
if(!r.perks) return
|
||||
if (!r.perks) return false;
|
||||
if ("mode" in r) {
|
||||
if (r.mode !== "short") {
|
||||
return false;
|
||||
|
@ -65,35 +67,32 @@ migrate("remove_long_and_creative_mode_data", () => {
|
|||
localStorage.setItem("breakout_71_runs_history", JSON.stringify(cleaned));
|
||||
});
|
||||
|
||||
|
||||
migrate("compact_runs_data", () => {
|
||||
let runsHistory = JSON.parse(
|
||||
localStorage.getItem("breakout_71_runs_history") || "[]",
|
||||
) as RunHistoryItem[];
|
||||
|
||||
runsHistory.forEach((r) => {
|
||||
r.runTime=Math.round(r.runTime)
|
||||
for(let key in r.perks){
|
||||
if(r.perks && !r.perks[key]){
|
||||
delete r.perks[key]
|
||||
r.runTime = Math.round(r.runTime);
|
||||
for (let key in r.perks) {
|
||||
if (r.perks && !r.perks[key]) {
|
||||
delete r.perks[key];
|
||||
}
|
||||
}
|
||||
if('best_level_score' in r) {
|
||||
delete r.best_level_score
|
||||
if ("best_level_score" in r) {
|
||||
delete r.best_level_score;
|
||||
}
|
||||
if('worst_level_score' in r) {
|
||||
delete r.worst_level_score
|
||||
if ("worst_level_score" in r) {
|
||||
delete r.worst_level_score;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory));
|
||||
});
|
||||
|
||||
|
||||
// Avoid a boot loop by setting the hash before reloading
|
||||
// We can't set the query string as it is used for other things
|
||||
if(migrationsRun && !window.location.hash){
|
||||
window.location.hash='#reloadAfterMigration'
|
||||
window.location.reload()
|
||||
}
|
||||
if (migrationsRun && !window.location.hash) {
|
||||
window.location.hash = "#reloadAfterMigration";
|
||||
window.location.reload();
|
||||
}
|
||||
|
|
|
@ -13,11 +13,12 @@ import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
|
|||
import { isOptionOn } from "./options";
|
||||
import { getHistory } from "./gameOver";
|
||||
import { getTotalScore } from "./settings";
|
||||
import { isStartingPerk } from "./startingPerks";
|
||||
|
||||
export function getRunLevels(params: RunParams) {
|
||||
const history = getHistory();
|
||||
const unlocked = allLevels.filter(
|
||||
(l, li) => !reasonLevelIsLocked(li, history),
|
||||
(l, li) => !reasonLevelIsLocked(li, history, false),
|
||||
);
|
||||
|
||||
const firstLevel = params?.level
|
||||
|
@ -29,13 +30,6 @@ export function getRunLevels(params: RunParams) {
|
|||
.filter((l) => l.name !== params?.levelToAvoid)
|
||||
.sort(() => Math.random() - 0.5);
|
||||
|
||||
console.log("getRunLevels", {
|
||||
params,
|
||||
history,
|
||||
unlocked,
|
||||
firstLevel,
|
||||
restInRandomOrder,
|
||||
});
|
||||
return firstLevel.concat(
|
||||
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
|
||||
);
|
||||
|
@ -124,7 +118,9 @@ export function newGameState(params: RunParams): GameState {
|
|||
resetBalls(gameState);
|
||||
|
||||
if (!sumOfValues(gameState.perks)) {
|
||||
const giftable = getPossibleUpgrades(gameState).filter((u) => u.giftable);
|
||||
const giftable = getPossibleUpgrades(gameState).filter((u) =>
|
||||
isStartingPerk(u),
|
||||
);
|
||||
const randomGift =
|
||||
(isOptionOn("easy") && "slow_down") ||
|
||||
giftable[Math.floor(Math.random() * giftable.length)].id;
|
||||
|
|
96
src/openScorePanel.ts
Normal file
96
src/openScorePanel.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { GameState } from "./types";
|
||||
import { asyncAlert } from "./asyncAlert";
|
||||
import { t } from "./i18n/i18n";
|
||||
import {
|
||||
getLevelUnlockCondition,
|
||||
levelsListHTMl,
|
||||
max_levels,
|
||||
pickedUpgradesHTMl,
|
||||
reasonLevelIsLocked,
|
||||
} from "./game_utils";
|
||||
import { getCreativeModeWarning, getHistory } from "./gameOver";
|
||||
import { pause } from "./game";
|
||||
import { allLevels, icons } from "./loadGameData";
|
||||
|
||||
export async function openScorePanel(gameState: GameState) {
|
||||
pause(true);
|
||||
|
||||
await asyncAlert({
|
||||
title: t("score_panel.title", {
|
||||
score: gameState.score,
|
||||
level: gameState.currentLevel + 1,
|
||||
max: max_levels(gameState),
|
||||
}),
|
||||
|
||||
content: [
|
||||
getCreativeModeWarning(gameState),
|
||||
pickedUpgradesHTMl(gameState),
|
||||
levelsListHTMl(gameState, gameState.currentLevel),
|
||||
getNearestUnlockHTML(gameState),
|
||||
gameState.rerolls
|
||||
? t("score_panel.rerolls_count", { rerolls: gameState.rerolls })
|
||||
: "",
|
||||
],
|
||||
allowClose: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function getNearestUnlockHTML(gameState: GameState) {
|
||||
const unlockable = allLevels
|
||||
.map((l, li) => {
|
||||
const { minScore, forbidden, required } = getLevelUnlockCondition(li);
|
||||
return {
|
||||
l,
|
||||
li,
|
||||
minScore,
|
||||
forbidden,
|
||||
required,
|
||||
missing: required.filter((u) => !gameState?.perks?.[u.id]),
|
||||
reason: reasonLevelIsLocked(li, getHistory(), false),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
({ reason, forbidden, missing }) =>
|
||||
// Level needs to be locked
|
||||
reason &&
|
||||
// we can't have a forbidden perk
|
||||
!forbidden.find((u) => gameState?.perks?.[u.id]) &&
|
||||
// All required upgrades need to be unlocked
|
||||
!missing.find((u) => u.threshold > gameState.totalScoreAtRunStart),
|
||||
);
|
||||
|
||||
const firstUnlockable =
|
||||
unlockable.find(({ missing }) => !missing.length) || unlockable[0];
|
||||
|
||||
if (!firstUnlockable) return "";
|
||||
let missingPoints = firstUnlockable.minScore - gameState.score;
|
||||
let missingUpgrades = firstUnlockable.missing.map((u) => u.name).join(", ");
|
||||
|
||||
const title =
|
||||
(missingUpgrades &&
|
||||
t("score_panel.get_upgrades_to_unlock", {
|
||||
missingUpgrades,
|
||||
points: missingPoints,
|
||||
level: firstUnlockable.l.name,
|
||||
})) ||
|
||||
(missingPoints > 0 &&
|
||||
t("score_panel.score_to_unlock", {
|
||||
points: missingPoints,
|
||||
level: firstUnlockable.l.name,
|
||||
})) ||
|
||||
t("score_panel.continue_to_unlock", {
|
||||
level: firstUnlockable.l.name,
|
||||
});
|
||||
|
||||
return `
|
||||
<p>${t("score_panel.close_to_unlock")}</p>
|
||||
<div class="upgrade used">
|
||||
${icons[firstUnlockable.l.name]}
|
||||
<p>
|
||||
<strong>${title}</strong>
|
||||
${firstUnlockable.reason?.text}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
`;
|
||||
}
|
|
@ -252,7 +252,6 @@ export function render(gameState: GameState) {
|
|||
coin.a,
|
||||
);
|
||||
});
|
||||
console.log(gameState.level.color);
|
||||
// Black shadow around balls
|
||||
if (!isOptionOn("basic")) {
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
|
@ -897,7 +896,6 @@ export function drawFuzzyBall(
|
|||
size / 2,
|
||||
);
|
||||
gradient.addColorStop(0, color);
|
||||
console.log(color);
|
||||
gradient.addColorStop(0.3, color + "88");
|
||||
gradient.addColorStop(0.6, color + "22");
|
||||
gradient.addColorStop(1, "transparent");
|
||||
|
|
|
@ -1,99 +1,117 @@
|
|||
import {getHistory} from "./gameOver";
|
||||
import {icons} from "./loadGameData";
|
||||
import {t} from "./i18n/i18n";
|
||||
import {asyncAlert} from "./asyncAlert";
|
||||
import {rawUpgrades} from "./upgrades";
|
||||
import { getHistory } from "./gameOver";
|
||||
import { icons } from "./loadGameData";
|
||||
import { t } from "./i18n/i18n";
|
||||
import { asyncAlert } from "./asyncAlert";
|
||||
import { rawUpgrades } from "./upgrades";
|
||||
|
||||
export function runHistoryViewerMenuEntry(){
|
||||
const history = getHistory()
|
||||
export function runHistoryViewerMenuEntry() {
|
||||
const history = getHistory();
|
||||
|
||||
return {
|
||||
icon:icons['icon:history'],
|
||||
text:t('history.title'),
|
||||
disabled : history.length<10,
|
||||
help: history.length<10 ? t('history.locked'):t('history.help',{count:history.length}),
|
||||
async value(){
|
||||
let sort = 0
|
||||
let sortDir = -1
|
||||
let columns = [
|
||||
{
|
||||
label:t('history.columns.started'),
|
||||
field: r=>r.started,
|
||||
render(v){
|
||||
return new Date(v).toISOString().slice(0,10)
|
||||
}
|
||||
},
|
||||
{
|
||||
label:t('history.columns.score'),
|
||||
field: r=>r.score
|
||||
},
|
||||
{
|
||||
label:t('history.columns.runTime'),
|
||||
tooltip:t('history.columns.runTime_tooltip'),
|
||||
return {
|
||||
icon: icons["icon:history"],
|
||||
text: t("history.title"),
|
||||
disabled: history.length < 10,
|
||||
help:
|
||||
history.length < 10
|
||||
? t("history.locked")
|
||||
: t("history.help", { count: history.length }),
|
||||
async value() {
|
||||
let sort = 0;
|
||||
let sortDir = -1;
|
||||
let columns = [
|
||||
{
|
||||
label: t("history.columns.started"),
|
||||
field: (r) => r.started,
|
||||
render(v) {
|
||||
return new Date(v).toISOString().slice(0, 10);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("history.columns.score"),
|
||||
field: (r) => r.score,
|
||||
},
|
||||
{
|
||||
label: t("history.columns.runTime"),
|
||||
tooltip: t("history.columns.runTime_tooltip"),
|
||||
|
||||
field: r=>r.runTime,
|
||||
render(v){
|
||||
return Math.floor(v/1000)+'s'
|
||||
}
|
||||
},
|
||||
{
|
||||
label:t('history.columns.puck_bounces'),
|
||||
tooltip:t('history.columns.puck_bounces_tooltip'),
|
||||
field: r=>r.puck_bounces,
|
||||
},
|
||||
{
|
||||
label:t('history.columns.max_combo'),
|
||||
field: r=>r.max_combo,
|
||||
},
|
||||
{
|
||||
label:t('history.columns.upgrades_picked'),
|
||||
field: r=>r.upgrades_picked,
|
||||
},
|
||||
...rawUpgrades.map(u=>({
|
||||
label: icons['icon:'+u.id],
|
||||
tooltip:u.name,
|
||||
field: r=>r.perks[u.id]||0,
|
||||
render(v){
|
||||
if(!v) return '-'
|
||||
return v
|
||||
}
|
||||
}))
|
||||
]
|
||||
while(true){
|
||||
const header = columns.map((c, ci) => `<th data-tooltip="${c.tooltip || ''}" data-resolve-to="sort:${ci}">${c.label}</th>`).join('')
|
||||
const toString = v => v.toString()
|
||||
const tbody = history.sort((a, b) => sortDir * (columns[sort].field(a) - columns[sort].field(b))).map(h => '<tr>' + columns.map(c => {
|
||||
const value = c.field(h) ?? 0
|
||||
const render = c.render || toString
|
||||
return '<td>' + render(value) + '</td>'
|
||||
}).join('') + '</tr>').join('')
|
||||
field: (r) => r.runTime,
|
||||
render(v) {
|
||||
return Math.floor(v / 1000) + "s";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("history.columns.puck_bounces"),
|
||||
tooltip: t("history.columns.puck_bounces_tooltip"),
|
||||
field: (r) => r.puck_bounces,
|
||||
},
|
||||
{
|
||||
label: t("history.columns.max_combo"),
|
||||
field: (r) => r.max_combo,
|
||||
},
|
||||
{
|
||||
label: t("history.columns.upgrades_picked"),
|
||||
field: (r) => r.upgrades_picked,
|
||||
},
|
||||
...rawUpgrades.map((u) => ({
|
||||
label: icons["icon:" + u.id],
|
||||
tooltip: u.name,
|
||||
field: (r) => r.perks?.[u.id] || 0,
|
||||
render(v) {
|
||||
if (!v) return "-";
|
||||
return v;
|
||||
},
|
||||
})),
|
||||
];
|
||||
while (true) {
|
||||
const header = columns
|
||||
.map(
|
||||
(c, ci) =>
|
||||
`<th data-tooltip="${c.tooltip || ""}" data-resolve-to="sort:${ci}">${c.label}</th>`,
|
||||
)
|
||||
.join("");
|
||||
const toString = (v) => v.toString();
|
||||
const tbody = history
|
||||
.sort(
|
||||
(a, b) =>
|
||||
sortDir * (columns[sort].field(a) - columns[sort].field(b)),
|
||||
)
|
||||
.map(
|
||||
(h) =>
|
||||
"<tr>" +
|
||||
columns
|
||||
.map((c) => {
|
||||
const value = c.field(h) ?? 0;
|
||||
const render = c.render || toString;
|
||||
return "<td>" + render(value) + "</td>";
|
||||
})
|
||||
.join("") +
|
||||
"</tr>",
|
||||
)
|
||||
.join("");
|
||||
|
||||
|
||||
const result = await asyncAlert({
|
||||
title: t('history.title'),
|
||||
className: 'history',
|
||||
content: [
|
||||
`
|
||||
const result = await asyncAlert({
|
||||
title: t("history.title"),
|
||||
className: "history",
|
||||
content: [
|
||||
`
|
||||
<table>
|
||||
<thead><tr>${header}</tr></thead>
|
||||
<tbody>${tbody}</tbody>
|
||||
</table>
|
||||
`
|
||||
|
||||
]
|
||||
})
|
||||
if(!result) return
|
||||
if(result.startsWith('sort:')){
|
||||
const newSort = parseInt(result.split(':')[1])
|
||||
if(newSort==sort){
|
||||
sortDir*=-1
|
||||
}else{
|
||||
sortDir=-1
|
||||
sort=newSort
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
});
|
||||
if (!result) return;
|
||||
if (result.startsWith("sort:")) {
|
||||
const newSort = parseInt(result.split(":")[1]);
|
||||
if (newSort == sort) {
|
||||
sortDir *= -1;
|
||||
} else {
|
||||
sortDir = -1;
|
||||
sort = newSort;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export function startingPerkMenuButton() {
|
|||
},
|
||||
};
|
||||
}
|
||||
function isChecked(u: Upgrade): boolean {
|
||||
export function isStartingPerk(u: Upgrade): boolean {
|
||||
return getSettingValue("start_with_" + u.id, u.giftable);
|
||||
}
|
||||
|
||||
|
@ -24,9 +24,9 @@ export async function openStartingPerksEditor() {
|
|||
(u) =>
|
||||
!u.requires && !["instant_upgrade"].includes(u.id) && u.threshold <= ts,
|
||||
);
|
||||
const starting = avaliable.filter((u) => isChecked(u));
|
||||
const starting = avaliable.filter((u) => isStartingPerk(u));
|
||||
const buttons = avaliable.map((u) => {
|
||||
const checked = isChecked(u);
|
||||
const checked = isStartingPerk(u);
|
||||
return {
|
||||
icon: u.icon,
|
||||
text: u.name,
|
||||
|
@ -48,7 +48,7 @@ export async function openStartingPerksEditor() {
|
|||
],
|
||||
});
|
||||
if (perk) {
|
||||
setSettingValue("start_with_" + perk.id, !isChecked(perk));
|
||||
setSettingValue("start_with_" + perk.id, !isStartingPerk(perk));
|
||||
openStartingPerksEditor();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -662,5 +662,5 @@ export const rawUpgrades = [
|
|||
name: t("upgrades.limitless.name"),
|
||||
help: (lvl: number) => t("upgrades.limitless.help", { lvl }),
|
||||
fullHelp: t("upgrades.limitless.fullHelp"),
|
||||
}
|
||||
},
|
||||
] as const;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue