Made the "combo lost" text last 500ms instead of the pointless 150ms

This commit is contained in:
Renan LE CARO 2025-03-29 17:40:07 +01:00
parent da89cdb647
commit 23798c4e58
9 changed files with 262 additions and 265 deletions

109
Readme.md
View file

@ -13,46 +13,6 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
- [GitLab](https://gitlab.com/lecarore/breakout71)
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
# Premium: allow looping
Allow players to loop the game :
- keep your score
- keep 1 perk, level it up beyond the max
- ban all other perks
- unlock all upgrades in loop 1+
Hard to scale :
- concave_puck
- instant_upgrade
- etherealcoins (0 grav, maybe then start floting like helium ? maybe less viscosity)
- shocks (maybe spawn balls during the explosion ? maybe bigger explosions for this)
- ghost_coins : pass through bricks will less friction ?
- clairvoyant
# Todo
- Make fullscreen an option and turn it back on when playing
- real time stats as the option says.
- weee sound when ball lost to side or sky
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
- people assume unbounded allows for wrap around
- coin magnet and viscosity : only one level ~2.5
- Boost Ascetism : give +2 or even +3 combo per brick destroyed
- wind : move coins based on puck movement not position
- show -N points in red when combo resets
- reach is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
- respawn: N% of bricks respawn after N seconds
- [jaceys] Move the restart button out of the menu, so that it is more easily accessible
- [jaceys] A visual indication of whether a ball has hit a brick this serve
- [obigre] Offer to level ups perks separately
- bring back detailed help of perks as "intel"
- https://weblate.org/fr/
# System requirements
Breakout 71 can work offline (add it to home screen) and perform well even on low-end devices.
@ -60,9 +20,68 @@ It's very lean and does not take much storage space (Roughly 0.1MB).
If the app stutters, turn on "fast mode" in the settings to render a simplified view that should be faster.
There's also an easy mode for kids (slower ball).
# Looping
# UX
Premium users can loop the game to continue player a harder and harder version of the game.
At the end of the last level of each run, they can start a new loop. They'll be taken back
to level 1, with only one of their perks, leveled up. All the other perks they used in the run
will be banned from the pool. The perk they decide to keep will gain one level, even if it was
already maxed out.
# Todo before next release
- b71 white border around dark coins
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
- wind : move coins based on puck movement not position
- show -N points in red when combo resets
- Top down /read: punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
- [jaceys] Move the restart button out of the menu, so that it is more easily accessible
- [jaceys] A visual indication of whether a ball has hit a brick this serve
- scale concave_puck
- scale instant_upgrade
- scale etherealcoins (0 grav, maybe then start floting like helium ? maybe less viscosity)
- scale shocks (maybe spawn balls during the explosion ? maybe bigger explosions for this)
- scale ghost_coins : pass through bricks will less friction ?
- scale clairvoyant
# 29 march 2025
- Removed all previous loop only hazards
- Looping now bans all your perks except one. That one can level up beyond the normal max.
- Adjusted many perks to work beyond the max
- Split list of perks and levels in unlocks
- Asceticism now gives +3 combo per lvl
- Fortunate ball has a stronger effect
- Bigger puck : puck can now cover the whole screen at higher levels, but not more
- Corner shot : higher levels let you move further away from the play area
- Forgiving : level 2 halves the penalty, level 3 is a third ..
- Helium : stronger anti gravity at higher levels
- Implosions : works like bigger-explosions at higher levels
- Metamorphosis : coins can stain more bricks at higher levels
- Re-spawn : now delay based and probabilistic, to scale more easily with higher levels. no need to hit the puck
- sacrifice : at level 2+ the combo is doubles/tripled just before clearing the screen of any bricks
- shunt : changed the math keep 25% of combo at level 1,50% at level 2,63% at level 3,70% at level 4..
- soft reset : same math as shunt
- smaller puck : now the puck can get as small as a ball
- Unbounded : at level 2+, the top of the level is gone too
- Make fullscreen an option and turn it back on when playing
- Made the "combo lost" text last 500ms instead of the pointless 150ms
# 28 march 2025
- loop : added red/blue coins (red kill you, blue freeze puck) (removed later)
- added more hazard that were then removed
- add a toggle to switch between the “coin” design and colored bubbles
# UX / gameplay
- on mobile, relative movement of the touch would be amplified and added to the puck
- option : don't pause on mobile when lifting finger
- [obigre] Offer to level ups perks separately
- bring back detailed help of perks as "intel"
- https://weblate.org/fr/
- strict sample size red borders ?
- add some tutorial-like hints
- It's a bit confusing at first to grasp that one upgrade is applied randomly at the start of the game. Offer instead to skip lvl 1 and directly pick 4 perks, but only if you manage to clear lvl 1 with 4 upgrades.
@ -95,7 +114,7 @@ There's also an easy mode for kids (slower ball).
- Overgrowth — when the ball touches a bomb brick it turns into a regular green brick and spawns 1 more bricks near it (additional levels spawn 2 additional bricks)
# graphics
- Waterline under the puck, coins slow down a lot, reflections
- webgl rendering: background gradient light map, shinier coins
- experiment with showing the combo somewhere else, maybe top center, maybe instead of score.
@ -201,12 +220,6 @@ There's also an easy mode for kids (slower ball).
- animals
- countries flags and shapes
# extra settings
- add a toggle to switch between the “coin” design and colored bubbles
- on mobile, relative movement of the touch would be amplified and added to the puck
- option : don't pause on mobile when lifting finger
# extend re-playability
- hard mode : bricks take many hits, perks more rare, missing clears level score, missing coins deducts score..

121
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -43,6 +43,7 @@ import {hashCode} from "./getLevelBackground";
import {premiumMenuEntry} from "./premium";
export function play() {
if (applyFullScreenChoice()) return;
if (gameState.running) return;
gameState.running = true;
gameState.ballStickToPuck = false;
@ -50,7 +51,6 @@ export function play() {
startRecordingGame(gameState);
getAudioContext()?.resume();
resumeRecording();
// document.body.classList[gameState.running ? 'add' : 'remove']('running')
}
@ -571,36 +571,12 @@ async function openSettingsMenu() {
help: options[key].help,
value: () => {
toggleOption(key);
if (key === "mobile-mode") fitSize();
fitSize();
applyFullScreenChoice()
openSettingsMenu();
},
});
}
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
if (document.fullscreenElement !== null) {
actions.push({
text: t("main_menu.fullscreen_exit"),
help: t("main_menu.fullscreen_exit_help"),
icon: icons["icon:exit_fullscreen"],
value() {
toggleFullScreen();
openSettingsMenu();
},
});
} else {
actions.push({
text: t("main_menu.fullscreen"),
help: t("main_menu.fullscreen_help"),
icon: icons["icon:fullscreen"],
value() {
toggleFullScreen();
openSettingsMenu();
},
});
}
}
actions.push({
text: t("main_menu.reset"),
help: t("main_menu.reset_help"),
@ -818,10 +794,43 @@ async function openSettingsMenu() {
}
}
function applyFullScreenChoice(): boolean {
try {
if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) {
return false
}
if (document.fullscreenElement !== null && !isOptionOn("fullscreen")) {
if (document.exitFullscreen) {
document.exitFullscreen();
return true
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
return true
}
} else if (isOptionOn("fullscreen") && !document.fullscreenElement) {
const docel = document.documentElement;
if (docel.requestFullscreen) {
docel.requestFullscreen();
return true
} else if (docel.webkitRequestFullscreen) {
docel.webkitRequestFullscreen();
return true
}
}
} catch (e) {
console.warn(e);
}
return false
}
async function openUnlocksList() {
const ts = getTotalScore();
const actions = [
...upgrades
const upgradeActions = upgrades
.sort((a, b) => a.threshold - b.threshold)
.map(({name, id, threshold, icon, help}) => ({
text: name,
@ -830,8 +839,9 @@ async function openUnlocksList() {
disabled: ts < threshold,
value: {perks: {[id]: 1}} as RunParams,
icon,
})),
...allLevels
}))
const levelActions = allLevels
.sort((a, b) => a.threshold - b.threshold)
.map((l) => {
const available = ts >= l.threshold;
@ -847,22 +857,21 @@ async function openUnlocksList() {
value: {level: l.name} as RunParams,
icon: icons[l.name],
};
}),
];
})
const percentUnlock = Math.round(
(actions.filter((a) => !a.disabled).length / actions.length) * 100,
([...upgradeActions, ...levelActions].filter((a) => !a.disabled).length / (upgradeActions.length +
levelActions.length)) * 100,
);
const tryOn = await asyncAlert<RunParams>({
title: t("unlocks.title", {percentUnlock}),
content: [
`<p>${t("unlocks.intro", {ts})}
`<p>${t("unlocks.intro", {ts, highScore: gameState.highScore})}
${percentUnlock < 100 ? t("unlocks.greyed_out_help") : ""}</p> `,
...actions,
`<p>
Your high score is ${gameState.highScore}.
Click an item above to start a run with it.
</p>`,
...upgradeActions,
t("unlocks.level"),
...levelActions,
],
allowClose: true,
actionsAsGrid: true,
@ -893,26 +902,6 @@ export async function confirmRestart(gameState) {
});
}
export function toggleFullScreen() {
try {
if (document.fullscreenElement !== null) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
}
} else {
const docel = document.documentElement;
if (docel.requestFullscreen) {
docel.requestFullscreen();
} else if (docel.webkitRequestFullscreen) {
docel.webkitRequestFullscreen();
}
}
} catch (e) {
console.warn(e);
}
}
const pressed: { [k: string]: number } = {
ArrowLeft: 0,
@ -931,7 +920,8 @@ export function setKeyPressed(key: string, on: 0 | 1) {
document.addEventListener("keydown", (e) => {
if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) {
toggleFullScreen();
toggleOption('fullscreen');
applyFullScreenChoice()
} else if (e.key in pressed) {
setKeyPressed(e.key, 1);
}
@ -988,13 +978,15 @@ export function restart(params: RunParams) {
setLevel(gameState, 0);
}
restart(
(window.location.search.includes("stressTest") && {
level: "Bird",
perks: {
pierce:1,
sapper:1,
implosions: 3
pierce: 1,
sapper: 1,
implosions: 3,
streak_shots:1
},
levelsPerLoop: 2,
}) ||

View file

@ -178,7 +178,8 @@ export function resetCombo(
);
}
if (typeof x !== "undefined" && typeof y !== "undefined") {
makeText(gameState, x, y, "red", "-" + lost, 20, 150);
makeText(gameState, x, y, "red", "-" + lost, 20, 500+clamp(lost, 0,500));
}
}
return lost;
@ -197,7 +198,7 @@ export function decreaseCombo(
if (lost) {
schedulGameSound(gameState, "comboDecrease", x, 1);
if (typeof x !== "undefined" && typeof y !== "undefined") {
makeText(gameState, x, y, "red", "-" + lost, 20, 300);
makeText(gameState, x, y, "red", "-" + lost, 20, 400+lost);
}
}
}
@ -1709,7 +1710,7 @@ function makeText(
color: colorString,
text: string,
size = 20,
duration = 150,
duration = 500,
) {
append(gameState.texts, (p: Partial<TextFlash>) => {
p.time = gameState.levelTime;
@ -1717,7 +1718,7 @@ function makeText(
p.y = y;
p.color = color;
p.size = size;
p.duration = duration;
p.duration = clamp(duration,400,2000);
p.text = text;
});
}

View file

@ -191,6 +191,8 @@ export function countBricksBelow(gameState: GameState, index: number) {
}
export function comboKeepingRate(level:number){
if(level<=0) return 0
return 1-1/(1+level)*1.5
return clamp(1-1/(1+level)*1.5,0,1)
}
for(let i = 0;i<5;i++){
console.log(Math.round(comboKeepingRate(i)*100)+'%')
}

View file

@ -837,36 +837,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>fullscreen_exit</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>fullscreen_exit_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>fullscreen_help</name>
<description/>
@ -2032,6 +2002,21 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>level</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>level_description</name>
<description/>

View file

@ -51,9 +51,7 @@
"main_menu.download_save_file_help": "Get a save file",
"main_menu.footer_html": "<p> \n<span>Made in France by <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> \n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donate</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> \n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Web version</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Privacy Policy</a>\n<span>v.{{appVersion}}</span>\n</p>\n",
"main_menu.fullscreen": "Fullscreen",
"main_menu.fullscreen_exit": "Exit Fullscreen",
"main_menu.fullscreen_exit_help": "Might not work on some machines",
"main_menu.fullscreen_help": "Might not work on some machines",
"main_menu.fullscreen_help": "Game will try to go full screen before starting",
"main_menu.kid": "Kids mode",
"main_menu.kid_help": "Start future runs with \"slower ball\".",
"main_menu.language": "Language",
@ -128,9 +126,10 @@
"score_panel.upcoming_levels": "Upcoming levels :",
"score_panel.upgrades_picked": "Upgrades picked so far : ",
"unlocks.greyed_out_help": "The greyed out ones can be unlocked by increasing your total score. The total score increases every time you score in game.",
"unlocks.intro": "Your total score is {{ts}}. Below are all the upgrades and levels the games has to offer.",
"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 run with it .Your high score is {{highScore}}.",
"unlocks.level": "Here are all the game levels, click one to start a run with that starting level. ",
"unlocks.level_description": "A {{size}}x{{size}} level with {{bricks}} bricks",
"unlocks.title": "You unlocked {{percentUnlock}}% of the game.",
"unlocks.title": "You unlocked {{percentUnlock}}% of the game. ",
"unlocks.unlocks_at": "Unlocks at total score {{threshold}}.",
"upgrades.asceticism.fullHelp": "You'll need to store the coins somewhere while your combo climbs. ",
"upgrades.asceticism.help": "+{{combo}} combo / brick, combo resets on coin catch",

View file

@ -51,9 +51,7 @@
"main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde",
"main_menu.footer_html": " <p> \n<span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span>\n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donner</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a>\n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> \n<span>v.{{appVersion}}</span>\n</p>",
"main_menu.fullscreen": "Plein écran",
"main_menu.fullscreen_exit": "Quitter le plein écran",
"main_menu.fullscreen_exit_help": "Ne fonctionne pas toujours",
"main_menu.fullscreen_help": "Ne fonctionne pas toujours",
"main_menu.fullscreen_help": "Le jeu essaiera de passer en plein écran quand vous le démarrez",
"main_menu.kid": "Mode enfants",
"main_menu.kid_help": "Balle plus lente",
"main_menu.language": "Langue",
@ -128,7 +126,8 @@
"score_panel.upcoming_levels": "Niveaux de la parties : ",
"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.",
"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.",
"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 commencer une nouvelle partie. ",
"unlocks.level": "Voci tous les niveaux du jeu. Cliquez sur un niveau pour commencer une nouvelle partie avec ce niveau de départ. ",
"unlocks.level_description": "Un niveau {{size}}x{{size}} avec {{bricks}} briques",
"unlocks.title": "Vous avez débloqué {{percentUnlock}}% du jeu.",
"unlocks.unlocks_at": "Déverrouillé au score total {{threshold}}.",

View file

@ -50,6 +50,11 @@ export const options = {
name: t("main_menu.record"),
help: t("main_menu.record_help"),
},
fullscreen: {
default: false,
name: t("main_menu.fullscreen"),
help: t("main_menu.fullscreen_help"),
},
} as const satisfies { [k: string]: OptionDef };
export function isOptionOn(key: OptionId) {