This commit is contained in:
Renan LE CARO 2025-04-08 10:36:30 +02:00
parent e1c20627bc
commit 6ef13f2d19
15 changed files with 289 additions and 65 deletions

View file

@ -39,11 +39,12 @@ New players get confused as to which upgrades they have and why a side became re
## To do ## To do
- Explain the combo
- As soon as level condition is reached, lock it in and tell the user - As soon as level condition is reached, lock it in and tell the user
- change fortunate ball to work more like coin magnet, carrying the balls around to catch them at next puck bounce
## Done ## Done
- extra life only saves your last ball, max 7 instead of 3
- Don't use "RAZ" in French explanations. - Don't use "RAZ" in French explanations.
- explain ghost coin's slow down effect - explain ghost coin's slow down effect
- when there are only a few coins, make them brighter - when there are only a few coins, make them brighter

154
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -512,3 +512,35 @@ h2.histogram-title strong {
background: white; background: white;
} }
} }
.toast {
position:fixed;
left:0;
top:40px;
display:flex;
align-items:center;
gap:10px;
opacity:0.8;
background:black;
border:1px solid white;
border-radius:2px;
padding-right:10px;
pointer-events: none;
animation: toast 800ms forwards;
}
@keyframes toast {
0%{
opacity: 0;
transform:translate(-20px, 0);
}
10%,90%{
opacity: 0.8;
transform: none;
}
100%{
opacity: 0;
transform:translate(20px, 0);
}
}

View file

@ -74,6 +74,7 @@ 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"; import { getNearestUnlockHTML, openScorePanel } from "./openScorePanel";
import {monitorLevelsUnlocks} from "./monitorLevelsUnlocks";
export async function play() { export async function play() {
if (await applyFullScreenChoice()) return; if (await applyFullScreenChoice()) return;
@ -422,6 +423,10 @@ setInterval(() => {
FPSCounter = 0; FPSCounter = 0;
}, 1000); }, 1000);
setInterval(() => {
monitorLevelsUnlocks(gameState)
}, 500);
window.addEventListener("visibilitychange", () => { window.addEventListener("visibilitychange", () => {
if (document.hidden) { if (document.hidden) {
pause(true); pause(true);

View file

@ -1508,7 +1508,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
ball.y > ylimit && ball.y > ylimit &&
ball.vy > 0 && ball.vy > 0 &&
(ballIsUnderPuck || (ballIsUnderPuck ||
(gameState.perks.extra_life && (gameState.balls.length<2 && gameState.perks.extra_life &&
ball.y > ylimit + gameState.puckHeight / 2)) ball.y > ylimit + gameState.puckHeight / 2))
) { ) {
if (ballIsUnderPuck) { if (ballIsUnderPuck) {

View file

@ -1,12 +1,4 @@
import { import {Ball, GameState, Level, PerkId, PerksMap, RunHistoryItem, UpgradeLike,} from "./types";
Ball,
GameState,
Level,
PerkId,
PerksMap,
RunHistoryItem,
Upgrade,
} from "./types";
import {icons, upgrades} from "./loadGameData"; import {icons, upgrades} from "./loadGameData";
import {t} from "./i18n/i18n"; import {t} from "./i18n/i18n";
import {clamp} from "./pure_functions"; import {clamp} from "./pure_functions";
@ -279,16 +271,10 @@ export function highScoreText() {
return ""; return "";
} }
type UpgradeLike = { id: PerkId; name: string; requires: string }; let excluded: Set<PerkId>;
function isExcluded(id:PerkId){
export function getLevelUnlockCondition(levelIndex: number) { if(!excluded) {
// Returns "" if level is unlocked, otherwise a string explaining how to unlock it excluded = new Set([
let required: UpgradeLike[] = [],
forbidden: UpgradeLike[] = [],
minScore = Math.max(-1000 + 100 * levelIndex, 0);
if (levelIndex > 20) {
const excluded: Set<PerkId> = new Set([
"extra_levels", "extra_levels",
"extra_life", "extra_life",
"one_more_choice", "one_more_choice",
@ -300,11 +286,22 @@ export function getLevelUnlockCondition(levelIndex: number) {
rawUpgrades.forEach((u) => { rawUpgrades.forEach((u) => {
if (u.requires) excluded.add(u.requires); if (u.requires) excluded.add(u.requires);
}); });
}
return excluded.has(id)
}
export function getLevelUnlockCondition(levelIndex: number) {
// Returns "" if level is unlocked, otherwise a string explaining how to unlock it
let required: UpgradeLike[] = [],
forbidden: UpgradeLike[] = [],
minScore = Math.max(-1000 + 100 * levelIndex, 0);
if (levelIndex > 20) {
const possibletargets = rawUpgrades const possibletargets = rawUpgrades
.slice(0, Math.floor(levelIndex / 2)) .slice(0, Math.floor(levelIndex / 2))
.map((u) => u) .map((u) => u)
.filter((u) => !excluded.has(u.id)) .filter((u) => !isExcluded(u.id))
.sort( .sort(
(a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id), (a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id),
); );

View file

@ -154,7 +154,7 @@
"score_panel.upgrades_picked": "Upgrades picked so far : ", "score_panel.upgrades_picked": "Upgrades picked so far : ",
"unlocks.greyed_out_help": "The grayed out upgrades can be unlocked by increasing your total score. The total score increases every time you score in game, outside of test runs.", "unlocks.greyed_out_help": "The grayed out upgrades can be unlocked by increasing your total score. The total score increases every time you score in game, outside of test runs.",
"unlocks.intro": "Your total score is {{ts}}. Below are all the upgrades and levels the games has to offer. Click an upgrade or level below to start a test game with it. Hint: you can set the starting upgrades in the settings.", "unlocks.intro": "Your total score is {{ts}}. Below are all the upgrades and levels the games has to offer. Click an upgrade or level below to start a test game with it. Hint: you can set the starting upgrades in the settings.",
"unlocks.just_unlocked": "You just unlocked a level", "unlocks.just_unlocked": "Level unlocked",
"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 test 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 test 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.",

View file

@ -154,7 +154,7 @@
"score_panel.upgrades_picked": "Améliorations choisies jusqu'à présent :", "score_panel.upgrades_picked": "Améliorations choisies jusqu'à présent :",
"unlocks.greyed_out_help": "Les éléments grisées peuvent être débloquées en augmentant votre score total. Le score total augmente à chaque fois que vous marquez des points dans le jeu, en dehors des parties de test.", "unlocks.greyed_out_help": "Les éléments grisées peuvent être débloquées en augmentant votre score total. Le score total augmente à chaque fois que vous marquez des points dans le jeu, en dehors des parties de test.",
"unlocks.intro": "Votre score total est de {{ts}}. Vous trouverez ci-dessous toutes les améliorations et tous les niveaux que le jeu peut offrir. Cliquez sur l'un d'entre eux pour les essayer dans une partie de test. Astuce : vous pouvez choisir les améliorations de départ dans les réglages.", "unlocks.intro": "Votre score total est de {{ts}}. Vous trouverez ci-dessous toutes les améliorations et tous les niveaux que le jeu peut offrir. Cliquez sur l'un d'entre eux pour les essayer dans une partie de test. Astuce : vous pouvez choisir les améliorations de départ dans les réglages.",
"unlocks.just_unlocked": "Vous venez de débloquer un niveau", "unlocks.just_unlocked": "Niveau débloqué",
"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 de test 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 de test 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.",

View file

@ -2,6 +2,8 @@ import { RunHistoryItem } from "./types";
import _appVersion from "./data/version.json"; import _appVersion from "./data/version.json";
import { generateSaveFileContent } from "./generateSaveFileContent"; import { generateSaveFileContent } from "./generateSaveFileContent";
import {getLevelUnlockCondition, reasonLevelIsLocked} from "./game_utils";
import {allLevels} from "./loadGameData";
// 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;
@ -17,6 +19,17 @@ function migrate(name: string, cb: () => void) {
} }
} }
} }
function afterMigration(){
// 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 = "";
}
}
migrate("save_data_before_upgrade_to_" + _appVersion, () => { migrate("save_data_before_upgrade_to_" + _appVersion, () => {
localStorage.setItem( localStorage.setItem(
@ -90,12 +103,25 @@ migrate("compact_runs_data", () => {
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 migrate("set_breakout_71_unlocked_levels"+_appVersion, () => {
// We can't set the query string as it is used for other things // We want to lock any level unlocked by an app upgrade too
if (migrationsRun && !window.location.hash) { let runsHistory = JSON.parse(
window.location.hash = "#reloadAfterMigration"; localStorage.getItem("breakout_71_runs_history") || "[]",
window.location.reload(); ) as RunHistoryItem[];
}
if (!migrationsRun) { let breakout_71_unlocked_levels = JSON.parse(
window.location.hash = ""; localStorage.getItem("breakout_71_unlocked_levels") || "[]",
) as string[];
allLevels.filter((l,li)=>!reasonLevelIsLocked(li,runsHistory,false)).forEach(l=>{
if(!breakout_71_unlocked_levels.includes(l.name)){
breakout_71_unlocked_levels.push(l.name)
} }
})
localStorage.setItem("breakout_71_unlocked_levels", JSON.stringify(breakout_71_unlocked_levels));
});
afterMigration()

View file

@ -0,0 +1,41 @@
import {GameState, UpgradeLike} from "./types";
import {getSettingValue, setSettingValue} from "./settings";
import {allLevels, icons} from "./loadGameData";
import { getLevelUnlockCondition} from "./game_utils";
import {t} from "./i18n/i18n";
import {toast} from "./toast";
import {schedulGameSound} from "./gameStateMutators";
let list: {minScore: number, forbidden: UpgradeLike[], required: UpgradeLike[]}[] ;
let unlocked=new Set(getSettingValue('breakout_71_unlocked_levels',[]) as string[])
export function monitorLevelsUnlocks(gameState:GameState){
if(gameState.creative) return;
if(!list){
list=allLevels.map((l,li)=>({
name:l.name,li,l,
...getLevelUnlockCondition(li)
}))
}
list.forEach(({name, minScore, forbidden, required, l})=>{
// Already unlocked
if(unlocked.has(name)) return
// Score not reached yet
if(gameState.score<minScore) return
// We are missing a required perk
if(required.find(id=>!gameState.perks[id])) return;
// We have a forbidden perk
if(forbidden.find(id=>gameState.perks[id])) return;
// Level just got unlocked
unlocked.add(name)
setSettingValue('breakout_71_unlocked_levels', getSettingValue('breakout_71_unlocked_levels',[]).concat([name]))
toast(icons[name]+'<strong>'+t('unlocks.just_unlocked')+'</strong>')
schedulGameSound(gameState, 'colorChange', 0, 1)
})
}

View file

@ -42,13 +42,14 @@ export function getRunLevels(
export function newGameState(params: RunParams): GameState { export function newGameState(params: RunParams): GameState {
const highScore = getHighScore(); const highScore = getHighScore();
const totalScoreAtRunStart=getTotalScore()
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) }; const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
let randomGift: PerkId | undefined = undefined; let randomGift: PerkId | undefined = undefined;
if (!sumOfValues(perks)) { if (!sumOfValues(perks)) {
const giftable = upgrades.filter( const giftable = upgrades.filter(
(u) => highScore >= u.threshold && !u.requires && isStartingPerk(u), (u) => totalScoreAtRunStart >= u.threshold && isStartingPerk(u),
); );
randomGift = randomGift =
@ -106,7 +107,8 @@ export function newGameState(params: RunParams): GameState {
ballSize: 20, ballSize: 20,
coinSize: 14, coinSize: 14,
puckHeight: 20, puckHeight: 20,
totalScoreAtRunStart: getTotalScore(),
totalScoreAtRunStart,
pauseUsesDuringRun: 0, pauseUsesDuringRun: 0,
keyboardPuckSpeed: 0, keyboardPuckSpeed: 0,
lastTick: performance.now(), lastTick: performance.now(),

View file

@ -17,6 +17,7 @@ export function startingPerkMenuButton() {
}; };
} }
export function isStartingPerk(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);
} }

8
src/toast.ts Normal file
View file

@ -0,0 +1,8 @@
export function toast(html){
const div =document.createElement('div')
div.classList='toast'
div.innerHTML=html
document.body.appendChild(div)
// TOast
// setTimeout(()=>div.remove() , 500 )
}

1
src/types.d.ts vendored
View file

@ -296,3 +296,4 @@ export type OptionDef = {
help: string; help: string;
}; };
export type OptionId = keyof typeof options; export type OptionId = keyof typeof options;
export type UpgradeLike = { id: PerkId; name: string; requires: string };

View file

@ -9,7 +9,7 @@ export const rawUpgrades = [
threshold: 0, threshold: 0,
giftable: false, giftable: false,
id: "extra_life", id: "extra_life",
max: 3, max: 7,
name: t("upgrades.extra_life.name"), name: t("upgrades.extra_life.name"),
help: (lvl: number) => help: (lvl: number) =>
lvl === 1 lvl === 1