mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-21 20:46:14 -04:00
wip
This commit is contained in:
parent
9624c5b351
commit
e78021ff83
24 changed files with 840 additions and 437 deletions
30
Readme.md
30
Readme.md
|
@ -11,18 +11,42 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
|
||||||
- [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout)
|
- [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout)
|
||||||
- [GitLab](https://gitlab.com/lecarore/breakout71)
|
- [GitLab](https://gitlab.com/lecarore/breakout71)
|
||||||
|
|
||||||
|
# Game issues and potential solutions
|
||||||
|
|
||||||
|
I should show what the starting perk is :
|
||||||
|
- make the perk icon playable as the first level of the run
|
||||||
|
- show it on screen for the first 5 seconds
|
||||||
|
|
||||||
|
When you have already a nice build and still get offered many perks, it gets tiring:
|
||||||
|
- limit all build to N perks (maybe could be boosted with a perk)
|
||||||
|
- add a "no more upgrade in the run, but double coins" perk
|
||||||
|
|
||||||
|
One play style is too OP, no reason to try other things
|
||||||
|
- encourage varied play style with level unlock requirements (testing)
|
||||||
|
- add loop run where user levels can't be used in further loops (boring)
|
||||||
|
- add lab mode where you need to make three builds (complex, lots of clicking, not fun)
|
||||||
|
|
||||||
|
Some upgrades currently are not really useful
|
||||||
|
- remove them
|
||||||
|
- add more upgrades to complement them
|
||||||
|
- force users to try them to unlock levels
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## To do
|
## To do
|
||||||
|
|
||||||
|
- maybe just make the starting perk icon the first level ? kind of silly, kind of fun.
|
||||||
- avoid showing a +1 and -1 at the same time when a combo increase is reset
|
- avoid showing a +1 and -1 at the same time when a combo increase is reset
|
||||||
- display runs history
|
|
||||||
- display closest unlock with current perks in score and gameover screens
|
- display closest unlock with current perks in score and gameover screens
|
||||||
- progress of unlock
|
- show the initial perk when we start a new game.
|
||||||
|
- "skip" option on the upgrades, for when you don't want any of them.
|
||||||
|
- fix starting perk option not working
|
||||||
|
|
||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
|
- progress bar for unlock in unlocks menu
|
||||||
|
- display runs history
|
||||||
- in the runs history, only save perks that were chosen by the user
|
- in the runs history, only save perks that were chosen by the user
|
||||||
- migration to save past content to localStorage.recovery_data right before starting a new version
|
- migration to save past content to localStorage.recovery_data right before starting a new version
|
||||||
- mention unlock conditions in help
|
- mention unlock conditions in help
|
||||||
|
@ -231,6 +255,8 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
|
||||||
- experiment with showing the combo somewhere else, maybe top center, maybe instead of score.
|
- experiment with showing the combo somewhere else, maybe top center, maybe instead of score.
|
||||||
|
|
||||||
## Easy perks ideas
|
## Easy perks ideas
|
||||||
|
- two for one : add a 2 for one upgrade combo to the choice lists
|
||||||
|
- cash out : double last level's gains
|
||||||
- snowball : Combo resets every 0.1s . +1 combo for each combo gained Since last reset.
|
- snowball : Combo resets every 0.1s . +1 combo for each combo gained Since last reset.
|
||||||
- Chain reaction : +lvl*lvl combo per brick broken by an explosion, combo resets after explosion is over
|
- Chain reaction : +lvl*lvl combo per brick broken by an explosion, combo resets after explosion is over
|
||||||
- coins doubled when touched by ball, lvl times, looks smaller and lighter
|
- coins doubled when touched by ball, lvl times, looks smaller and lighter
|
||||||
|
|
|
@ -29,8 +29,8 @@ android {
|
||||||
applicationId = "me.lecaro.breakout"
|
applicationId = "me.lecaro.breakout"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 29065815
|
versionCode = 29067102
|
||||||
versionName = "29065815"
|
versionName = "29067102"
|
||||||
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
408
dist/index.html
vendored
408
dist/index.html
vendored
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
||||||
// The version of the cache.
|
// The version of the cache.
|
||||||
const VERSION = "29065815";
|
const VERSION = "29067102";
|
||||||
|
|
||||||
// The name of the cache
|
// The name of the cache
|
||||||
const CACHE_NAME = `breakout-71-${VERSION}`;
|
const CACHE_NAME = `breakout-71-${VERSION}`;
|
||||||
|
|
|
@ -43,7 +43,7 @@ export async function asyncAlert<t>({
|
||||||
content: (string | AsyncAlertAction<t>)[];
|
content: (string | AsyncAlertAction<t>)[];
|
||||||
allowClose?: boolean;
|
allowClose?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}): Promise<t | void|string> {
|
}): Promise<t | void> {
|
||||||
updateAlertsOpen(+1);
|
updateAlertsOpen(+1);
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
popupWrap.className = className;
|
popupWrap.className = className;
|
||||||
|
@ -139,11 +139,16 @@ ${icon}
|
||||||
addto.appendChild(button);
|
addto.appendChild(button);
|
||||||
});
|
});
|
||||||
|
|
||||||
popup.addEventListener('click', e=>{
|
popup.addEventListener(
|
||||||
if(e.target.getAttribute('data-resolve-to')){
|
"click",
|
||||||
closeWithResult(e.target.getAttribute('data-resolve-to'))
|
(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.appendChild(popup);
|
||||||
(
|
(
|
||||||
popupWrap.querySelector(
|
popupWrap.querySelector(
|
||||||
|
|
|
@ -75,7 +75,8 @@ export async function openCreativeModePerksPicker() {
|
||||||
})),
|
})),
|
||||||
t("lab.select_level"),
|
t("lab.select_level"),
|
||||||
...allLevels.map((l, li) => {
|
...allLevels.map((l, li) => {
|
||||||
const problem = reasonLevelIsLocked(li, getHistory());
|
const problem =
|
||||||
|
reasonLevelIsLocked(li, getHistory(), true)?.text || "";
|
||||||
return {
|
return {
|
||||||
icon: icons[l.name],
|
icon: icons[l.name],
|
||||||
text: l.name,
|
text: l.name,
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
"29065815"
|
"29067102"
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
* {
|
* {
|
||||||
font-family: Courier New,
|
font-family:
|
||||||
Courier,
|
Courier New,
|
||||||
Lucida Sans Typewriter,
|
Courier,
|
||||||
Lucida Typewriter,
|
Lucida Sans Typewriter,
|
||||||
monospace;
|
Lucida Typewriter,
|
||||||
|
monospace;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,27 +470,45 @@ h2.histogram-title strong {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
|
||||||
table {
|
table {
|
||||||
th:hover{
|
th:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
td, th {
|
td,
|
||||||
|
th {
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
th:first-child, td:first-child {
|
th:first-child,
|
||||||
text-align: left
|
td:first-child {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 { allLevels, appVersion, icons, upgrades } from "./loadGameData";
|
||||||
import {Ball, Coin, GameState, LightFlash, OptionId, ParticleFlash, PerkId, RunParams, TextFlash,} from "./types";
|
import {
|
||||||
import {getAudioContext, playPendingSounds} from "./sounds";
|
Ball,
|
||||||
|
Coin,
|
||||||
|
GameState,
|
||||||
|
LightFlash,
|
||||||
|
OptionId,
|
||||||
|
ParticleFlash,
|
||||||
|
PerkId,
|
||||||
|
RunParams,
|
||||||
|
TextFlash,
|
||||||
|
} from "./types";
|
||||||
|
import { getAudioContext, playPendingSounds } from "./sounds";
|
||||||
import {
|
import {
|
||||||
currentLevelInfo,
|
currentLevelInfo,
|
||||||
describeLevel,
|
describeLevel,
|
||||||
|
@ -13,7 +23,7 @@ import {
|
||||||
} from "./game_utils";
|
} from "./game_utils";
|
||||||
|
|
||||||
import "./PWA/sw_loader";
|
import "./PWA/sw_loader";
|
||||||
import {getCurrentLang, t} from "./i18n/i18n";
|
import { getCurrentLang, t } from "./i18n/i18n";
|
||||||
import {
|
import {
|
||||||
cycleMaxCoins,
|
cycleMaxCoins,
|
||||||
cycleMaxParticles,
|
cycleMaxParticles,
|
||||||
|
@ -30,21 +40,40 @@ import {
|
||||||
setLevel,
|
setLevel,
|
||||||
setMousePos,
|
setMousePos,
|
||||||
} from "./gameStateMutators";
|
} from "./gameStateMutators";
|
||||||
import {backgroundCanvas, gameCanvas, haloCanvas, haloScale, render, scoreDisplay,} from "./render";
|
import {
|
||||||
import {pauseRecording, recordOneFrame, resumeRecording, startRecordingGame,} from "./recording";
|
backgroundCanvas,
|
||||||
import {newGameState} from "./newGameState";
|
gameCanvas,
|
||||||
import {alertsOpen, asyncAlert, AsyncAlertAction, closeModal, requiredAsyncAlert,} from "./asyncAlert";
|
haloCanvas,
|
||||||
import {isOptionOn, options, toggleOption} from "./options";
|
haloScale,
|
||||||
import {hashCode} from "./getLevelBackground";
|
render,
|
||||||
import {hoursSpentPlaying} from "./pure_functions";
|
scoreDisplay,
|
||||||
import {helpMenuEntry} from "./help";
|
} from "./render";
|
||||||
import {creativeMode} from "./creative";
|
import {
|
||||||
import {setupTooltips} from "./tooltip";
|
pauseRecording,
|
||||||
import {startingPerkMenuButton} from "./startingPerks";
|
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 "./migrations";
|
||||||
import {getCreativeModeWarning, getHistory} from "./gameOver";
|
import { getHistory } from "./gameOver";
|
||||||
import {generateSaveFileContent} from "./generateSaveFileContent";
|
import { generateSaveFileContent } from "./generateSaveFileContent";
|
||||||
import {runHistoryViewerMenuEntry} from "./runHistoryViewer";
|
import { runHistoryViewerMenuEntry } from "./runHistoryViewer";
|
||||||
|
import { getNearestUnlockHTML, openScorePanel } from "./openScorePanel";
|
||||||
|
|
||||||
export async function play() {
|
export async function play() {
|
||||||
if (await applyFullScreenChoice()) return;
|
if (await applyFullScreenChoice()) return;
|
||||||
|
@ -267,6 +296,8 @@ export async function openUpgradesPicker(gameState: GameState) {
|
||||||
<p>${levelsListHTMl(gameState, gameState.currentLevel + 1)}</p>
|
<p>${levelsListHTMl(gameState, gameState.currentLevel + 1)}</p>
|
||||||
`,
|
`,
|
||||||
...actions,
|
...actions,
|
||||||
|
|
||||||
|
getNearestUnlockHTML(gameState),
|
||||||
pickedUpgradesHTMl(gameState),
|
pickedUpgradesHTMl(gameState),
|
||||||
|
|
||||||
`<div id="level-recording-container"></div>`,
|
`<div id="level-recording-container"></div>`,
|
||||||
|
@ -400,7 +431,7 @@ window.addEventListener("visibilitychange", () => {
|
||||||
scoreDisplay.addEventListener("click", (e) => {
|
scoreDisplay.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!alertsOpen) {
|
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(
|
(document.getElementById("menu") as HTMLButtonElement).addEventListener(
|
||||||
"click",
|
"click",
|
||||||
(e) => {
|
(e) => {
|
||||||
|
@ -461,7 +470,7 @@ export async function openMainMenu() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
creativeMode(gameState),
|
creativeMode(gameState),
|
||||||
runHistoryViewerMenuEntry(),
|
runHistoryViewerMenuEntry(),
|
||||||
{
|
{
|
||||||
icon: icons["icon:unlocks"],
|
icon: icons["icon:unlocks"],
|
||||||
text: t("main_menu.unlocks"),
|
text: t("main_menu.unlocks"),
|
||||||
|
@ -568,8 +577,7 @@ async function openSettingsMenu() {
|
||||||
text: t("main_menu.download_save_file"),
|
text: t("main_menu.download_save_file"),
|
||||||
help: t("main_menu.download_save_file_help"),
|
help: t("main_menu.download_save_file_help"),
|
||||||
async value() {
|
async value() {
|
||||||
|
const signedPayload = generateSaveFileContent();
|
||||||
const signedPayload =generateSaveFileContent()
|
|
||||||
|
|
||||||
const dlLink = document.createElement("a");
|
const dlLink = document.createElement("a");
|
||||||
|
|
||||||
|
@ -796,13 +804,17 @@ async function openUnlocksList() {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const levelActions = allLevels.map((l, li) => {
|
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 {
|
return {
|
||||||
text: l.name,
|
text: l.name + percentUnlocked,
|
||||||
disabled: !!problem,
|
disabled: !!lockedBecause,
|
||||||
value: { level: l.name } as RunParams,
|
value: { level: l.name } as RunParams,
|
||||||
icon: icons[l.name],
|
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) => ({
|
.map((l, li) => ({
|
||||||
li,
|
li,
|
||||||
l,
|
l,
|
||||||
r: reasonLevelIsLocked(li, runsHistory),
|
r: reasonLevelIsLocked(li, runsHistory, false)?.text,
|
||||||
}))
|
}))
|
||||||
.filter((l) => l.r);
|
.filter((l) => l.r);
|
||||||
|
|
||||||
|
gameState.runStatistics.runTime = Math.round(
|
||||||
gameState.runStatistics.runTime=Math.round(gameState.runStatistics.runTime)
|
gameState.runStatistics.runTime,
|
||||||
const perks={...gameState.perks}
|
);
|
||||||
for(let id in perks){
|
const perks = { ...gameState.perks };
|
||||||
if(!perks[id]){
|
for (let id in perks) {
|
||||||
delete perks[id]
|
if (!perks[id]) {
|
||||||
|
delete perks[id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runsHistory.push({
|
runsHistory.push({
|
||||||
|
@ -144,7 +145,7 @@ export function getHistograms(gameState: GameState) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const unlocked = locked.filter(
|
const unlocked = locked.filter(
|
||||||
({ li }) => !reasonLevelIsLocked(li, runsHistory),
|
({ li }) => !reasonLevelIsLocked(li, runsHistory, true),
|
||||||
);
|
);
|
||||||
if (unlocked.length) {
|
if (unlocked.length) {
|
||||||
unlockedLevels = `
|
unlockedLevels = `
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
PerkId,
|
PerkId,
|
||||||
PerksMap,
|
PerksMap,
|
||||||
RunHistoryItem,
|
RunHistoryItem,
|
||||||
|
Upgrade,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { icons, upgrades } from "./loadGameData";
|
import { icons, upgrades } from "./loadGameData";
|
||||||
import { t } from "./i18n/i18n";
|
import { t } from "./i18n/i18n";
|
||||||
|
@ -278,51 +279,96 @@ export function highScoreText() {
|
||||||
return "";
|
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(
|
export function reasonLevelIsLocked(
|
||||||
levelIndex: number,
|
levelIndex: number,
|
||||||
history: RunHistoryItem[],
|
history: RunHistoryItem[],
|
||||||
) {
|
mentionBestScore: boolean,
|
||||||
// Returns "" if level is unlocked, otherwise a string explaining how to unlock it
|
): null | { reached: number; minScore: number; text: string } {
|
||||||
if (levelIndex <= 10) {
|
const { required, forbidden, minScore } = getLevelUnlockCondition(levelIndex);
|
||||||
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",
|
|
||||||
];
|
|
||||||
|
|
||||||
const possibletargets = rawUpgrades
|
const reached = getBestScoreMatching(history, required, forbidden);
|
||||||
.slice(0, Math.floor(levelIndex / 2))
|
let reachedText =
|
||||||
.map((u) => u)
|
reached && mentionBestScore ? t("unlocks.reached", { reached }) : "";
|
||||||
.filter((u) => !excluded.includes(u.id))
|
if (reached >= minScore) {
|
||||||
.sort((a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id));
|
return null;
|
||||||
|
} else if (!required.length && !forbidden.length) {
|
||||||
const length = Math.ceil(levelIndex / 30);
|
return {
|
||||||
const required = possibletargets.slice(0, length);
|
reached,
|
||||||
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", {
|
|
||||||
minScore,
|
minScore,
|
||||||
required: required.map((u) => u.name).join(", "),
|
text: t("unlocks.minScore", { minScore }) + reachedText,
|
||||||
forbidden: forbidden.map((u) => u.name).join(", "),
|
};
|
||||||
});
|
} 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() {
|
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;
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
// Avoid including recovery info in the recovery info
|
||||||
const key = localStorage.key(i) as string;
|
if (["recovery_data"].includes(key)) continue;
|
||||||
// Avoid including recovery info in the recovery info
|
const value = localStorage.getItem(key) as string;
|
||||||
if(['recovery_data'].includes(key)) continue
|
localStorageContent[key] = value;
|
||||||
const value = localStorage.getItem(key) as string;
|
}
|
||||||
localStorageContent[key] = value;
|
return JSON.stringify(localStorageContent);
|
||||||
}
|
|
||||||
return JSON.stringify(localStorageContent);
|
|
||||||
}
|
}
|
|
@ -2247,6 +2247,51 @@
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>score_panel</name>
|
<name>score_panel</name>
|
||||||
<children>
|
<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>
|
<concept_node>
|
||||||
<name>rerolls_count</name>
|
<name>rerolls_count</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
@ -2262,6 +2307,21 @@
|
||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</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>
|
<concept_node>
|
||||||
<name>title</name>
|
<name>title</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
@ -2462,6 +2522,21 @@
|
||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</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>
|
<concept_node>
|
||||||
<name>title_upgrades</name>
|
<name>title_upgrades</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
"main_menu.contrast": "High Contrast",
|
"main_menu.contrast": "High Contrast",
|
||||||
"main_menu.contrast_help": "More colorful and dark rendering",
|
"main_menu.contrast_help": "More colorful and dark rendering",
|
||||||
"main_menu.credit_levels": "<h2>Levels source or reference link</h2>",
|
"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": "You've played for {{hours}} hours",
|
||||||
"main_menu.donate_help": "How about donating {{suggestion}} € ? You can hide this reminder in the settings. ",
|
"main_menu.donate_help": "How about donating {{suggestion}} € ? You can hide this reminder in the settings. ",
|
||||||
"main_menu.donation_reminder": "Remind me to donate",
|
"main_menu.donation_reminder": "Remind me to donate",
|
||||||
|
@ -143,7 +143,11 @@
|
||||||
"play.stats.levelMisses": "Missed shots, where you hit nothing",
|
"play.stats.levelMisses": "Missed shots, where you hit nothing",
|
||||||
"play.stats.levelTime": "Level time",
|
"play.stats.levelTime": "Level time",
|
||||||
"play.stats.levelWallBounces": "Wall bounces",
|
"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.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": "{{score}} points at level {{level}}/{{max}} ",
|
||||||
"score_panel.title_looped": "{{score}} points at level {{level}}/{{max}} of loop {{loop}}",
|
"score_panel.title_looped": "{{score}} points at level {{level}}/{{max}} of loop {{loop}}",
|
||||||
"score_panel.upcoming_levels": "Upcoming levels :",
|
"score_panel.upcoming_levels": "Upcoming levels :",
|
||||||
|
@ -154,9 +158,10 @@
|
||||||
"unlocks.just_unlocked_plural": "You just unlocked {{count}} levels",
|
"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": "<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.level_description": "A {{size}}x{{size}} level with {{bricks}} bricks, {{colors}} colors and {{bombs}} bombs.",
|
||||||
"unlocks.minScore": "Reach ${{minScore}}",
|
"unlocks.minScore": "Reach ${{minScore}} in a run to unlock.",
|
||||||
"unlocks.minScoreWithPerks": "Reach ${{minScore}} in a run with {{required}} but without {{forbidden}}",
|
"unlocks.minScoreWithPerks": "Reach ${{minScore}} in a run with {{required}} but without {{forbidden}} to unlock.",
|
||||||
"unlocks.minTotalScore": "Accumulate a total of ${{score}}",
|
"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}}",
|
"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.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. ",
|
"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.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.summary": "Cette partie est terminée. Vous avez accumulé {{score}} pièces. ",
|
||||||
"gameOver.win.title": "Vous avez terminé cette partie",
|
"gameOver.win.title": "Vous avez terminé cette partie",
|
||||||
"history.columns.max_combo": "",
|
"history.columns.max_combo": "Combo maximum",
|
||||||
"history.columns.max_level": "",
|
"history.columns.max_level": "Les niveaux",
|
||||||
"history.columns.puck_bounces": "",
|
"history.columns.puck_bounces": "PB",
|
||||||
"history.columns.puck_bounces_tooltip": "",
|
"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": "Dur.",
|
||||||
"history.columns.runTime_tooltip": "",
|
"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": "",
|
"history.columns.score": "Score",
|
||||||
"history.columns.started": "",
|
"history.columns.started": "Date",
|
||||||
"history.columns.upgrades_picked": "",
|
"history.columns.upgrades_picked": "Mises à niveau",
|
||||||
"history.help": "",
|
"history.help": "Voir la liste de votre jeu {{count}} ",
|
||||||
"history.locked": "",
|
"history.locked": "Jouez d'abord au moins dix parties",
|
||||||
"history.title": "",
|
"history.title": "Historique",
|
||||||
"lab.help": "Essayez n'importe quel build",
|
"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.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",
|
"lab.menu_entry": "Mode créatif",
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
"main_menu.contrast": "Contraste élevé",
|
"main_menu.contrast": "Contraste élevé",
|
||||||
"main_menu.contrast_help": "Affichage plus contrasté et coloré",
|
"main_menu.contrast_help": "Affichage plus contrasté et coloré",
|
||||||
"main_menu.credit_levels": "<h2>Source ou référence des niveaux</h2>",
|
"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": "Vous avez joué {{hours}} heures",
|
||||||
"main_menu.donate_help": "Pourriez-vous donner {{suggestion}} € ? Vous pouvez masquer ce rappel dans les paramètres.",
|
"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",
|
"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.levelMisses": "Tirs ratés, ou vous n'avez touché aucune brique",
|
||||||
"play.stats.levelTime": "Durée du niveau",
|
"play.stats.levelTime": "Durée du niveau",
|
||||||
"play.stats.levelWallBounces": "Rebonds sur les murs",
|
"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.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": "{{score}} points au niveau {{level}}/{{max}} ",
|
||||||
"score_panel.title_looped": "{{score}} points au niveau {{level}}/{{max}} ",
|
"score_panel.title_looped": "{{score}} points au niveau {{level}}/{{max}} ",
|
||||||
"score_panel.upcoming_levels": "Niveaux de la parties : ",
|
"score_panel.upcoming_levels": "Niveaux de la parties : ",
|
||||||
|
@ -154,9 +158,10 @@
|
||||||
"unlocks.just_unlocked_plural": "Vous venez de débloquer {{count}} niveaux",
|
"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": "<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.level_description": "Un niveau {{size}}x{{size}} avec {{bricks}} briques, {{colors}} couleurs et {{bombs}} bombes.",
|
||||||
"unlocks.minScore": "Atteindre ${{minScore}}",
|
"unlocks.minScore": "Atteignez un score de ${{minScore}} dans une partie pour débloquer.",
|
||||||
"unlocks.minScoreWithPerks": "Atteignez ${{minScore}} dans une course avec {{required}} mais sans {{forbidden}}",
|
"unlocks.minScoreWithPerks": "Atteignez ${{minScore}} dans une partie avec {{required}} mais sans {{forbidden}}.",
|
||||||
"unlocks.minTotalScore": "Accumuler un total de ${{score}}",
|
"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}}",
|
"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.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",
|
"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.help": "Les explosions aspirent les pièces au lieu de les faire exploser.",
|
||||||
"upgrades.implosions.name": "Implosions",
|
"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.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.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.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",
|
"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": "La dernière brique s'autodétruit.",
|
||||||
"upgrades.skip_last.help_plural": "Les {{lvl}} dernières briques restantes s'autodétruiront",
|
"upgrades.skip_last.help_plural": "Les {{lvl}} dernières briques restantes s'autodétruiront",
|
||||||
"upgrades.skip_last.name": "Nettoyage facile",
|
"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.help": "La balle se déplace plus lentement",
|
||||||
"upgrades.slow_down.name": "Balle lente",
|
"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.",
|
"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 { RunHistoryItem } from "./types";
|
||||||
|
|
||||||
import _appVersion from "./data/version.json";
|
import _appVersion from "./data/version.json";
|
||||||
import {generateSaveFileContent} from "./generateSaveFileContent";
|
import { generateSaveFileContent } from "./generateSaveFileContent";
|
||||||
|
|
||||||
// The page will be reloaded if any migrations were run
|
// The page will be reloaded if any migrations were run
|
||||||
let migrationsRun=0
|
let migrationsRun = 0;
|
||||||
function migrate(name: string, cb: () => void) {
|
function migrate(name: string, cb: () => void) {
|
||||||
if (!localStorage.getItem(name)) {
|
if (!localStorage.getItem(name)) {
|
||||||
try {
|
try {
|
||||||
cb();
|
cb();
|
||||||
console.debug("Ran migration : " + name);
|
console.debug("Ran migration : " + name);
|
||||||
localStorage.setItem(name, "" + Date.now());
|
localStorage.setItem(name, "" + Date.now());
|
||||||
migrationsRun++
|
migrationsRun++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Migration " + name + " failed : ", e);
|
console.warn("Migration " + name + " failed : ", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate("save_data_before_upgrade_to_"+_appVersion, () => {
|
migrate("save_data_before_upgrade_to_" + _appVersion, () => {
|
||||||
localStorage.setItem("recovery_data",JSON.stringify(generateSaveFileContent()));
|
localStorage.setItem(
|
||||||
|
"recovery_data",
|
||||||
|
JSON.stringify(generateSaveFileContent()),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
migrate("migrate_high_scores", () => {
|
migrate("migrate_high_scores", () => {
|
||||||
const old = localStorage.getItem("breakout-3-hs");
|
const old = localStorage.getItem("breakout-3-hs");
|
||||||
if (old) {
|
if (old) {
|
||||||
|
@ -53,7 +55,7 @@ migrate("remove_long_and_creative_mode_data", () => {
|
||||||
) as RunHistoryItem[];
|
) as RunHistoryItem[];
|
||||||
|
|
||||||
let cleaned = runsHistory.filter((r) => {
|
let cleaned = runsHistory.filter((r) => {
|
||||||
if(!r.perks) return
|
if (!r.perks) return false;
|
||||||
if ("mode" in r) {
|
if ("mode" in r) {
|
||||||
if (r.mode !== "short") {
|
if (r.mode !== "short") {
|
||||||
return false;
|
return false;
|
||||||
|
@ -65,35 +67,32 @@ migrate("remove_long_and_creative_mode_data", () => {
|
||||||
localStorage.setItem("breakout_71_runs_history", JSON.stringify(cleaned));
|
localStorage.setItem("breakout_71_runs_history", JSON.stringify(cleaned));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
migrate("compact_runs_data", () => {
|
migrate("compact_runs_data", () => {
|
||||||
let runsHistory = JSON.parse(
|
let runsHistory = JSON.parse(
|
||||||
localStorage.getItem("breakout_71_runs_history") || "[]",
|
localStorage.getItem("breakout_71_runs_history") || "[]",
|
||||||
) as RunHistoryItem[];
|
) as RunHistoryItem[];
|
||||||
|
|
||||||
runsHistory.forEach((r) => {
|
runsHistory.forEach((r) => {
|
||||||
r.runTime=Math.round(r.runTime)
|
r.runTime = Math.round(r.runTime);
|
||||||
for(let key in r.perks){
|
for (let key in r.perks) {
|
||||||
if(r.perks && !r.perks[key]){
|
if (r.perks && !r.perks[key]) {
|
||||||
delete r.perks[key]
|
delete r.perks[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if('best_level_score' in r) {
|
if ("best_level_score" in r) {
|
||||||
delete r.best_level_score
|
delete r.best_level_score;
|
||||||
}
|
}
|
||||||
if('worst_level_score' in r) {
|
if ("worst_level_score" in r) {
|
||||||
delete r.worst_level_score
|
delete r.worst_level_score;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory));
|
localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Avoid a boot loop by setting the hash before reloading
|
// Avoid a boot loop by setting the hash before reloading
|
||||||
// We can't set the query string as it is used for other things
|
// We can't set the query string as it is used for other things
|
||||||
if(migrationsRun && !window.location.hash){
|
if (migrationsRun && !window.location.hash) {
|
||||||
window.location.hash='#reloadAfterMigration'
|
window.location.hash = "#reloadAfterMigration";
|
||||||
window.location.reload()
|
window.location.reload();
|
||||||
}
|
}
|
|
@ -13,11 +13,12 @@ import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
|
||||||
import { isOptionOn } from "./options";
|
import { isOptionOn } from "./options";
|
||||||
import { getHistory } from "./gameOver";
|
import { getHistory } from "./gameOver";
|
||||||
import { getTotalScore } from "./settings";
|
import { getTotalScore } from "./settings";
|
||||||
|
import { isStartingPerk } from "./startingPerks";
|
||||||
|
|
||||||
export function getRunLevels(params: RunParams) {
|
export function getRunLevels(params: RunParams) {
|
||||||
const history = getHistory();
|
const history = getHistory();
|
||||||
const unlocked = allLevels.filter(
|
const unlocked = allLevels.filter(
|
||||||
(l, li) => !reasonLevelIsLocked(li, history),
|
(l, li) => !reasonLevelIsLocked(li, history, false),
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstLevel = params?.level
|
const firstLevel = params?.level
|
||||||
|
@ -29,13 +30,6 @@ export function getRunLevels(params: RunParams) {
|
||||||
.filter((l) => l.name !== params?.levelToAvoid)
|
.filter((l) => l.name !== params?.levelToAvoid)
|
||||||
.sort(() => Math.random() - 0.5);
|
.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
console.log("getRunLevels", {
|
|
||||||
params,
|
|
||||||
history,
|
|
||||||
unlocked,
|
|
||||||
firstLevel,
|
|
||||||
restInRandomOrder,
|
|
||||||
});
|
|
||||||
return firstLevel.concat(
|
return firstLevel.concat(
|
||||||
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
|
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
|
||||||
);
|
);
|
||||||
|
@ -124,7 +118,9 @@ export function newGameState(params: RunParams): GameState {
|
||||||
resetBalls(gameState);
|
resetBalls(gameState);
|
||||||
|
|
||||||
if (!sumOfValues(gameState.perks)) {
|
if (!sumOfValues(gameState.perks)) {
|
||||||
const giftable = getPossibleUpgrades(gameState).filter((u) => u.giftable);
|
const giftable = getPossibleUpgrades(gameState).filter((u) =>
|
||||||
|
isStartingPerk(u),
|
||||||
|
);
|
||||||
const randomGift =
|
const randomGift =
|
||||||
(isOptionOn("easy") && "slow_down") ||
|
(isOptionOn("easy") && "slow_down") ||
|
||||||
giftable[Math.floor(Math.random() * giftable.length)].id;
|
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,
|
coin.a,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
console.log(gameState.level.color);
|
|
||||||
// Black shadow around balls
|
// Black shadow around balls
|
||||||
if (!isOptionOn("basic")) {
|
if (!isOptionOn("basic")) {
|
||||||
ctx.globalCompositeOperation = "source-over";
|
ctx.globalCompositeOperation = "source-over";
|
||||||
|
@ -897,7 +896,6 @@ export function drawFuzzyBall(
|
||||||
size / 2,
|
size / 2,
|
||||||
);
|
);
|
||||||
gradient.addColorStop(0, color);
|
gradient.addColorStop(0, color);
|
||||||
console.log(color);
|
|
||||||
gradient.addColorStop(0.3, color + "88");
|
gradient.addColorStop(0.3, color + "88");
|
||||||
gradient.addColorStop(0.6, color + "22");
|
gradient.addColorStop(0.6, color + "22");
|
||||||
gradient.addColorStop(1, "transparent");
|
gradient.addColorStop(1, "transparent");
|
||||||
|
|
|
@ -1,99 +1,117 @@
|
||||||
import {getHistory} from "./gameOver";
|
import { getHistory } from "./gameOver";
|
||||||
import {icons} from "./loadGameData";
|
import { icons } 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 { rawUpgrades } from "./upgrades";
|
||||||
|
|
||||||
export function runHistoryViewerMenuEntry(){
|
export function runHistoryViewerMenuEntry() {
|
||||||
const history = getHistory()
|
const history = getHistory();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icon:icons['icon:history'],
|
icon: icons["icon:history"],
|
||||||
text:t('history.title'),
|
text: t("history.title"),
|
||||||
disabled : history.length<10,
|
disabled: history.length < 10,
|
||||||
help: history.length<10 ? t('history.locked'):t('history.help',{count:history.length}),
|
help:
|
||||||
async value(){
|
history.length < 10
|
||||||
let sort = 0
|
? t("history.locked")
|
||||||
let sortDir = -1
|
: t("history.help", { count: history.length }),
|
||||||
let columns = [
|
async value() {
|
||||||
{
|
let sort = 0;
|
||||||
label:t('history.columns.started'),
|
let sortDir = -1;
|
||||||
field: r=>r.started,
|
let columns = [
|
||||||
render(v){
|
{
|
||||||
return new Date(v).toISOString().slice(0,10)
|
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.score"),
|
||||||
label:t('history.columns.runTime'),
|
field: (r) => r.score,
|
||||||
tooltip:t('history.columns.runTime_tooltip'),
|
},
|
||||||
|
{
|
||||||
|
label: t("history.columns.runTime"),
|
||||||
|
tooltip: t("history.columns.runTime_tooltip"),
|
||||||
|
|
||||||
field: r=>r.runTime,
|
field: (r) => r.runTime,
|
||||||
render(v){
|
render(v) {
|
||||||
return Math.floor(v/1000)+'s'
|
return Math.floor(v / 1000) + "s";
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label:t('history.columns.puck_bounces'),
|
label: t("history.columns.puck_bounces"),
|
||||||
tooltip:t('history.columns.puck_bounces_tooltip'),
|
tooltip: t("history.columns.puck_bounces_tooltip"),
|
||||||
field: r=>r.puck_bounces,
|
field: (r) => r.puck_bounces,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label:t('history.columns.max_combo'),
|
label: t("history.columns.max_combo"),
|
||||||
field: r=>r.max_combo,
|
field: (r) => r.max_combo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label:t('history.columns.upgrades_picked'),
|
label: t("history.columns.upgrades_picked"),
|
||||||
field: r=>r.upgrades_picked,
|
field: (r) => r.upgrades_picked,
|
||||||
},
|
},
|
||||||
...rawUpgrades.map(u=>({
|
...rawUpgrades.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,
|
||||||
render(v){
|
render(v) {
|
||||||
if(!v) return '-'
|
if (!v) return "-";
|
||||||
return v
|
return v;
|
||||||
}
|
},
|
||||||
}))
|
})),
|
||||||
]
|
];
|
||||||
while(true){
|
while (true) {
|
||||||
const header = columns.map((c, ci) => `<th data-tooltip="${c.tooltip || ''}" data-resolve-to="sort:${ci}">${c.label}</th>`).join('')
|
const header = columns
|
||||||
const toString = v => v.toString()
|
.map(
|
||||||
const tbody = history.sort((a, b) => sortDir * (columns[sort].field(a) - columns[sort].field(b))).map(h => '<tr>' + columns.map(c => {
|
(c, ci) =>
|
||||||
const value = c.field(h) ?? 0
|
`<th data-tooltip="${c.tooltip || ""}" data-resolve-to="sort:${ci}">${c.label}</th>`,
|
||||||
const render = c.render || toString
|
)
|
||||||
return '<td>' + render(value) + '</td>'
|
.join("");
|
||||||
}).join('') + '</tr>').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({
|
||||||
const result = await asyncAlert({
|
title: t("history.title"),
|
||||||
title: t('history.title'),
|
className: "history",
|
||||||
className: 'history',
|
content: [
|
||||||
content: [
|
`
|
||||||
`
|
|
||||||
<table>
|
<table>
|
||||||
<thead><tr>${header}</tr></thead>
|
<thead><tr>${header}</tr></thead>
|
||||||
<tbody>${tbody}</tbody>
|
<tbody>${tbody}</tbody>
|
||||||
</table>
|
</table>
|
||||||
`
|
`,
|
||||||
|
],
|
||||||
]
|
});
|
||||||
})
|
if (!result) return;
|
||||||
if(!result) return
|
if (result.startsWith("sort:")) {
|
||||||
if(result.startsWith('sort:')){
|
const newSort = parseInt(result.split(":")[1]);
|
||||||
const newSort = parseInt(result.split(':')[1])
|
if (newSort == sort) {
|
||||||
if(newSort==sort){
|
sortDir *= -1;
|
||||||
sortDir*=-1
|
} else {
|
||||||
}else{
|
sortDir = -1;
|
||||||
sortDir=-1
|
sort = newSort;
|
||||||
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);
|
return getSettingValue("start_with_" + u.id, u.giftable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,9 +24,9 @@ export async function openStartingPerksEditor() {
|
||||||
(u) =>
|
(u) =>
|
||||||
!u.requires && !["instant_upgrade"].includes(u.id) && u.threshold <= ts,
|
!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 buttons = avaliable.map((u) => {
|
||||||
const checked = isChecked(u);
|
const checked = isStartingPerk(u);
|
||||||
return {
|
return {
|
||||||
icon: u.icon,
|
icon: u.icon,
|
||||||
text: u.name,
|
text: u.name,
|
||||||
|
@ -48,7 +48,7 @@ export async function openStartingPerksEditor() {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (perk) {
|
if (perk) {
|
||||||
setSettingValue("start_with_" + perk.id, !isChecked(perk));
|
setSettingValue("start_with_" + perk.id, !isStartingPerk(perk));
|
||||||
openStartingPerksEditor();
|
openStartingPerksEditor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -662,5 +662,5 @@ export const rawUpgrades = [
|
||||||
name: t("upgrades.limitless.name"),
|
name: t("upgrades.limitless.name"),
|
||||||
help: (lvl: number) => t("upgrades.limitless.help", { lvl }),
|
help: (lvl: number) => t("upgrades.limitless.help", { lvl }),
|
||||||
fullHelp: t("upgrades.limitless.fullHelp"),
|
fullHelp: t("upgrades.limitless.fullHelp"),
|
||||||
}
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue