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

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

View file

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

View file

@ -1,17 +1,9 @@
import {
Ball,
GameState,
Level,
PerkId,
PerksMap,
RunHistoryItem,
Upgrade,
} from "./types";
import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { clamp } from "./pure_functions";
import { rawUpgrades } from "./upgrades";
import { hashCode } from "./getLevelBackground";
import {Ball, GameState, Level, PerkId, PerksMap, RunHistoryItem, UpgradeLike,} from "./types";
import {icons, upgrades} from "./loadGameData";
import {t} from "./i18n/i18n";
import {clamp} from "./pure_functions";
import {rawUpgrades} from "./upgrades";
import {hashCode} from "./getLevelBackground";
export function describeLevel(level: Level) {
let bricks = 0,
@ -279,16 +271,10 @@ 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 = Math.max(-1000 + 100 * levelIndex, 0);
if (levelIndex > 20) {
const excluded: Set<PerkId> = new Set([
let excluded: Set<PerkId>;
function isExcluded(id:PerkId){
if(!excluded) {
excluded = new Set([
"extra_levels",
"extra_life",
"one_more_choice",
@ -300,11 +286,22 @@ export function getLevelUnlockCondition(levelIndex: number) {
rawUpgrades.forEach((u) => {
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
.slice(0, Math.floor(levelIndex / 2))
.map((u) => u)
.filter((u) => !excluded.has(u.id))
.filter((u) => !isExcluded(u.id))
.sort(
(a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id),
);

View file

@ -154,7 +154,7 @@
"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.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.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.",

View file

@ -154,7 +154,7 @@
"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.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.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.",

View file

@ -2,6 +2,8 @@ import { RunHistoryItem } from "./types";
import _appVersion from "./data/version.json";
import { generateSaveFileContent } from "./generateSaveFileContent";
import {getLevelUnlockCondition, reasonLevelIsLocked} from "./game_utils";
import {allLevels} from "./loadGameData";
// The page will be reloaded if any migrations were run
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, () => {
localStorage.setItem(
@ -90,12 +103,25 @@ migrate("compact_runs_data", () => {
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 = "";
}
migrate("set_breakout_71_unlocked_levels"+_appVersion, () => {
// We want to lock any level unlocked by an app upgrade too
let runsHistory = JSON.parse(
localStorage.getItem("breakout_71_runs_history") || "[]",
) as RunHistoryItem[];
let breakout_71_unlocked_levels = JSON.parse(
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 {
const highScore = getHighScore();
const totalScoreAtRunStart=getTotalScore()
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
let randomGift: PerkId | undefined = undefined;
if (!sumOfValues(perks)) {
const giftable = upgrades.filter(
(u) => highScore >= u.threshold && !u.requires && isStartingPerk(u),
(u) => totalScoreAtRunStart >= u.threshold && isStartingPerk(u),
);
randomGift =
@ -106,7 +107,8 @@ export function newGameState(params: RunParams): GameState {
ballSize: 20,
coinSize: 14,
puckHeight: 20,
totalScoreAtRunStart: getTotalScore(),
totalScoreAtRunStart,
pauseUsesDuringRun: 0,
keyboardPuckSpeed: 0,
lastTick: performance.now(),

View file

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

5
src/types.d.ts vendored
View file

@ -1,5 +1,5 @@
import { rawUpgrades } from "./upgrades";
import { options } from "./options";
import {rawUpgrades} from "./upgrades";
import {options} from "./options";
export type colorString = string;
@ -296,3 +296,4 @@ export type OptionDef = {
help: string;
};
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,
giftable: false,
id: "extra_life",
max: 3,
max: 7,
name: t("upgrades.extra_life.name"),
help: (lvl: number) =>
lvl === 1