mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-20 12:15:06 -04:00
wip
This commit is contained in:
parent
e1c20627bc
commit
6ef13f2d19
15 changed files with 289 additions and 65 deletions
|
@ -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
154
dist/index.html
vendored
File diff suppressed because one or more lines are too long
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
|
@ -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()
|
41
src/monitorLevelsUnlocks.ts
Normal file
41
src/monitorLevelsUnlocks.ts
Normal 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)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
|
|
@ -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
8
src/toast.ts
Normal 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
1
src/types.d.ts
vendored
|
@ -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 };
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue