This commit is contained in:
Renan LE CARO 2025-04-30 09:44:14 +02:00
parent 4c66cc820c
commit cee5c6bc60
28 changed files with 948 additions and 344 deletions

View file

@ -13,25 +13,25 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
# Changelog
## To do
- instead of bouncing the ball,loosing a life pauses the game (with coins still in the air)
- rewoks perks choices
- goal : limit perk fatigue, avoid wall of texts, clarify challenges, allow users to skip
- remove rerolls
## Done
- rewoked perks choices to limit perk fatigue, avoid wall of texts, allow users to skip
- removed rerolls
- offer to pick 1 upgrade out of 3 choices
- playing well adds 1 upgrade and 1 choice
- playing even better adds 1 choice
- "more choices" perk adds 1 choice
- you can skip the upgrades and they'll be saved for later
- you can pick an upgrade multiple time to level it up
- you can skip the upgrades and they'll be saved for later as extra lives
- you can pick an upgrade multiple time to level it up
- bigger "level X of Y cleared"
- continue to level X/Y as button
- lives = upgrade points
- clarify challenges
- missed challenges show as greyed out choices (with unlock condition).
- bigger "level X or Y cleared", continue to level X/Y as button
- lives = upgrade points
- instead of bouncing the ball,loosing a life pauses the game (with coins still in the air)
## Done
- somehow score clicks didn't register while the game was playing, that's solved
- upgrades list now uses numbers instead of bars, looks better with limitless
- somehow score clicks didn't register while the game was playing, that's solved
- Fix : click tooltip to open on mobile, click anywhere to close
- Can't press help buttons in Creative Menu

View file

@ -29,8 +29,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
versionCode = 29097764
versionName = "29097764"
versionCode = 29099215
versionName = "29099215"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true

File diff suppressed because one or more lines are too long

127
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
// The version of the cache.
const VERSION = "29097764";
const VERSION = "29099215";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -101,8 +101,8 @@ export async function asyncAlert<t>({
popup.appendChild(addto);
}
const buttonWrap = document.createElement("div")
addto.appendChild(buttonWrap)
const buttonWrap = document.createElement("div");
addto.appendChild(buttonWrap);
const {
text,
@ -136,7 +136,9 @@ ${icon}
}
button.className =
className + (lastClickedItemIndex === index ? " needs-focus" : "")+' choice-button';
className +
(lastClickedItemIndex === index ? " needs-focus" : "") +
" choice-button";
buttonWrap.appendChild(button);

View file

@ -1392,66 +1392,71 @@
"credit": "Suggested by Big Goober. The red ghost, Blinky, from the arcade game \"Pac Man\""
},
{
"color": "#000000",
"name": "Fish",
"size": 11,
"bricks": "______________________________________________bbbb______tttttt___btgttbttt_bbtttttbtttb___ttbttt_bb_tttttt___b___________",
"name": "Fish",
"credit": "A fish based on the fish discord emoji. Suggested by Big Goober. "
},
{
"color": "#115988",
"name": "Spider",
"size": 7,
"bricks": "_l_____Sgg____ggSgBB_gSgBBBBSgggggg_gg___g_g_g_g_",
"name": "Spider",
"credit": "Suggested by obigre."
},
{
"color": "#115988",
"name": "Gliders",
"size": 8,
"bricks": "g_g______gg___l__g__l_l______ll__c________cc__W__cc____W_____WWW",
"name": "Gliders",
"credit": "iSuggested by obigre. Inspired by Conway's gliders"
},
{
"color": "#000000",
"name": "Lone island",
"size": 8,
"bricks": "C__________kkk____kkOkk___kkkO_k_k_k_O_______O______CC__tttCCCCt",
"name": "Lone island",
"credit": "Suggested by obigre. Which game would you take there ?"
},
{
"color": "#000000",
"name": "Spacewyrm Jon",
"size": 8,
"bricks": "___PPP____PPPP____SSSP____WPWP_P__PPP_PP___PP_____yPPy__bWWyyWWb",
"name": "Spacewyrm Jon",
"credit": "Suggested by obigre. The invertebrate hero with a gun"
},
{
"color": "#115988",
"name": "Taijitu",
"size": 7,
"bricks": "_WWWWW_W__WWWWgg__WBWggg_WWWgBg__WWgggg__g_ggggg_",
"name": "Taijitu",
"credit": "Suggested by obigre. Yin and yang fishes"
},
{
"color": "#115988",
"name": "Egg pan",
"size": 5,
"bricks": "WWWWgWWyWggWWWggggg____g_",
"name": "Egg pan",
"credit": "Suggested by obigre. Fried and tasty"
},
{
"color": "#000000",
"name": "Inception",
"size": 20,
"bricks": "____llllllllllll________lbbbbbb____l________lbbbbbb____l________l____bbtt__l________l____bbtt__l________l__bbtttt__l________l__bbtttt__l________l______tt__l________l____y_tt__l________l______ttttl________l_____yttttl________l__W_______l________l_____y____l________l__y_y_____l________l_y___y_y__l________l__________l________l___WWW____l________llllllllllll____________________________________________",
"name": "Inception",
"credit": "Breakout 71 within Breakout 71. By Noodlemire"
},
{
"color": "#000000",
"name": "Chess",
"size": 21,
"bricks": "_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_",
"name": "Chess",
"credit": "White n black by Topenvy"
},
{
"name": "icon:locked",
"size": 8,
"bricks": "__eeee____e__e____e__e____e__e___llllll__llllll__llllll__llllll_",
"svg": null,
"color": ""
},
{
"color": "#000000",
"size": 8,
"bricks": "_________GGWWrr__GGWWrr__GGWWrr__GGWWrr_________________________",
"name": "italy",
"credit": "italia by Topenvy"
}
]

View file

@ -767,5 +767,10 @@
"required": ["respawn", "wrap_left", "sapper"],
"forbidden": ["shocks", "metamorphosis", "pierce"],
"minScore": 14200
},
"italy": {
"required": ["sticky_coins", "pierce_color", "left_is_lava"],
"forbidden": ["transparency", "etherealcoins", "three_cushion"],
"minScore": 14300
}
}

View file

@ -1 +1 @@
"29097764"
"29099215"

View file

@ -1,9 +1,10 @@
* {
font-family: Courier New,
Courier,
Lucida Sans Typewriter,
Lucida Typewriter,
monospace;
font-family:
Courier New,
Courier,
Lucida Sans Typewriter,
Lucida Typewriter,
monospace;
box-sizing: border-box;
}
@ -73,7 +74,6 @@ canvas:not(#game) {
line-height: 20px;
box-shadow: 0 2px #fff;
}
}
#score {
@ -210,7 +210,6 @@ body:not(.has-alert-open) #popup {
opacity: 0.2;
}
}
}
> button[data-help-content] {
@ -425,7 +424,7 @@ h2.histogram-title strong {
.level {
color: #000;
background: #FFF;
background: #fff;
border-radius: 3px;
overflow: hidden;
font-size: 12px;
@ -435,7 +434,7 @@ h2.histogram-title strong {
position: relative;
//top: -3px;
font-weight: bold;
border: 1px solid #FFF;
border: 1px solid #fff;
//margin-left: 5px;
> span {
@ -443,38 +442,34 @@ h2.histogram-title strong {
position: relative;
&:first-child {
padding: 3px 6px 0 2px;
color: #000;
background: #FFF;
background: #fff;
}
&:last-child {
padding: 3px 3px 0 2px;
color: #FFF;
color: #fff;
background: #000;
&:before {
content: "";
display: block;
background: black;;
background: black;
position: absolute;
left: -2px;
top: 0;
bottom: 0;
width: 4px;
transform: skewX(-10deg)
transform: skewX(-10deg);
}
}
}
&.capped {
> span:first-child {
color: #FFF;
color: #fff;
background: #000;
}
> span:last-child::before {
@ -511,16 +506,22 @@ h2.histogram-title strong {
font-weight: bold;
padding: 5px;
border-radius: 5px;
box-shadow: 0 4px 0 gold, 0 4px 10px black;
box-shadow:
0 4px 0 gold,
0 4px 10px black;
border: 2px solid white;
cursor: pointer;
text-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
user-select: none;
&:active{
transform: translate(0,4px);
box-shadow: 0 0px 0 gold, 0 0px 10px black;
user-select: none;
&:active {
transform: translate(0, 4px);
box-shadow:
0 0px 0 gold,
0 0px 10px black;
}
transition: transform 0.2s, box-shadow 0.2s;
transition:
transform 0.2s,
box-shadow 0.2s;
}
}
@ -614,8 +615,9 @@ h2.histogram-title strong {
border-radius: 2px;
padding-right: 10px;
pointer-events: none;
transition: opacity 200ms,
transform 200ms;
transition:
opacity 200ms,
transform 200ms;
z-index: 7;
&.hidden {

View file

@ -1,6 +1,22 @@
import {allLevels, allLevelsAndIcons, appVersion, icons, upgrades,} from "./loadGameData";
import {Ball, Coin, GameState, LightFlash, OptionId, ParticleFlash, PerksMap, RunParams, TextFlash,} from "./types";
import {getAudioContext, playPendingSounds} from "./sounds";
import {
allLevels,
allLevelsAndIcons,
appVersion,
icons,
upgrades,
} from "./loadGameData";
import {
Ball,
Coin,
GameState,
LightFlash,
OptionId,
ParticleFlash,
PerksMap,
RunParams,
TextFlash,
} from "./types";
import { getAudioContext, playPendingSounds } from "./sounds";
import {
currentLevelInfo,
describeLevel,
@ -12,7 +28,7 @@ import {
} from "./game_utils";
import "./PWA/sw_loader";
import {getCurrentLang, languages, t} from "./i18n/i18n";
import { getCurrentLang, languages, t } from "./i18n/i18n";
import {
commitSettingsChangesToLocalStorage,
cycleMaxCoins,
@ -30,25 +46,42 @@ import {
setLevel,
setMousePos,
} from "./gameStateMutators";
import {backgroundCanvas, gameCanvas, getHaloScale, haloCanvas, render, scoreDisplay,} from "./render";
import {pauseRecording, recordOneFrame, resumeRecording, startRecordingGame,} from "./recording";
import {newGameState} from "./newGameState";
import {alertsOpen, asyncAlert, AsyncAlertAction, closeModal,} from "./asyncAlert";
import {isOptionOn, options, toggleOption} from "./options";
import {clamp, miniMarkDown,} from "./pure_functions";
import {helpMenuEntry} from "./help";
import {creativeMode} from "./creative";
import {hideAnyTooltip, setupTooltips} from "./tooltip";
import {startingPerkMenuButton} from "./startingPerks";
import {
backgroundCanvas,
gameCanvas,
getHaloScale,
haloCanvas,
render,
scoreDisplay,
} from "./render";
import {
pauseRecording,
recordOneFrame,
resumeRecording,
startRecordingGame,
} from "./recording";
import { newGameState } from "./newGameState";
import {
alertsOpen,
asyncAlert,
AsyncAlertAction,
closeModal,
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options";
import { clamp, miniMarkDown } from "./pure_functions";
import { helpMenuEntry } from "./help";
import { creativeMode } from "./creative";
import { hideAnyTooltip, setupTooltips } from "./tooltip";
import { startingPerkMenuButton } from "./startingPerks";
import "./migrations";
import {getHistory} from "./gameOver";
import {generateSaveFileContent} from "./generateSaveFileContent";
import {runHistoryViewerMenuEntry} from "./runHistoryViewer";
import {openScorePanel} from "./openScorePanel";
import {monitorLevelsUnlocks} from "./monitorLevelsUnlocks";
import {levelEditorMenuEntry} from "./levelEditor";
import {categories} from "./upgrades";
import {reasonLevelIsLocked} from "./get_level_unlock_condition";
import { getHistory } from "./gameOver";
import { generateSaveFileContent } from "./generateSaveFileContent";
import { runHistoryViewerMenuEntry } from "./runHistoryViewer";
import { openScorePanel } from "./openScorePanel";
import { monitorLevelsUnlocks } from "./monitorLevelsUnlocks";
import { levelEditorMenuEntry } from "./levelEditor";
import { categories } from "./upgrades";
import { reasonLevelIsLocked } from "./get_level_unlock_condition";
export async function play() {
if (await applyFullScreenChoice()) return;
@ -369,43 +402,37 @@ setInterval(() => {
monitorLevelsUnlocks(gameState);
}, 500);
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
pause(true);
}
});
if(getSettingValue('score-opened',0 )<3){
scoreDisplay.classList.add('button-look')
if (getSettingValue("score-opened", 0) < 3) {
scoreDisplay.classList.add("button-look");
}
const menuDisplay = document.getElementById("menu") as HTMLButtonElement;
if(getSettingValue('menu-opened',0 )<3){
menuDisplay.classList.add('button-look')
if (getSettingValue("menu-opened", 0) < 3) {
menuDisplay.classList.add("button-look");
}
function scoreOpen(e){
e.preventDefault();
function scoreOpen(e) {
e.preventDefault();
if (!alertsOpen) {
setSettingValue('score-opened',getSettingValue('score-opened',0 )+1 )
setSettingValue("score-opened", getSettingValue("score-opened", 0) + 1);
openScorePanel(gameState);
}
}
scoreDisplay.addEventListener("click",scoreOpen);
scoreDisplay.addEventListener("click", scoreOpen);
scoreDisplay.addEventListener("mousedown", scoreOpen);
menuDisplay.addEventListener(
"click",
(e) => {
e.preventDefault();
if (!alertsOpen) {
setSettingValue('menu-opened',getSettingValue('menu-opened',0 )+1 )
openMainMenu();
}
},
);
menuDisplay.addEventListener("click", (e) => {
e.preventDefault();
if (!alertsOpen) {
setSettingValue("menu-opened", getSettingValue("menu-opened", 0) + 1);
openMainMenu();
}
});
export const creativeModeThreshold = Math.max(
...upgrades.map((u) => u.threshold),

View file

@ -29,18 +29,31 @@ import {
telekinesisEffectRate,
yoyoEffectRate,
} from "./game_utils";
import {t} from "./i18n/i18n";
import { t } from "./i18n/i18n";
import {getCurrentMaxCoins, getCurrentMaxParticles} from "./settings";
import {background} from "./render";
import {gameOver} from "./gameOver";
import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, pause, startComputerControlledGame,} from "./game";
import {stopRecording} from "./recording";
import {isOptionOn} from "./options";
import {ballTransparency, clamp, coinsBoostedCombo, comboKeepingRate,} from "./pure_functions";
import {addToTotalScore} from "./addToTotalScore";
import {hashCode} from "./getLevelBackground";
import {openUpgradesPicker} from "./openUpgradesPicker";
import { getCurrentMaxCoins, getCurrentMaxParticles } from "./settings";
import { background } from "./render";
import { gameOver } from "./gameOver";
import {
brickIndex,
fitSize,
gameState,
hasBrick,
hitsSomething,
pause,
startComputerControlledGame,
} from "./game";
import { stopRecording } from "./recording";
import { isOptionOn } from "./options";
import {
ballTransparency,
clamp,
coinsBoostedCombo,
comboKeepingRate,
} from "./pure_functions";
import { addToTotalScore } from "./addToTotalScore";
import { hashCode } from "./getLevelBackground";
import { openUpgradesPicker } from "./openUpgradesPicker";
export function setMousePos(gameState: GameState, x: number) {
if (gameState.startParams.computer_controlled) return;
@ -606,7 +619,6 @@ export async function setLevel(gameState: GameState, l: number) {
gameState.currentLevel = l;
gameState.level = gameState.runLevels[l % gameState.runLevels.length];
if (l > 0) {
await openUpgradesPicker(gameState);
}
@ -956,8 +968,9 @@ export function gameStateTick(
}
}
if (( window.location.search.includes("skipplaying") ||
remainingBricks <= gameState.perks.skip_last) &&
if (
(window.location.search.includes("skipplaying") ||
remainingBricks <= gameState.perks.skip_last) &&
!gameState.autoCleanUses
) {
gameState.bricks.forEach((type, index) => {

View file

@ -1,9 +1,17 @@
import {Ball, Coin, GameState, Level, PerkId, PerksMap, Upgrade} from "./types";
import {icons, upgrades} from "./loadGameData";
import {t} from "./i18n/i18n";
import {clamp} from "./pure_functions";
import {getSettingValue, getTotalScore} from "./settings";
import {isOptionOn} from "./options";
import {
Ball,
Coin,
GameState,
Level,
PerkId,
PerksMap,
Upgrade,
} from "./types";
import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { clamp } from "./pure_functions";
import { getSettingValue, getTotalScore } from "./settings";
import { isOptionOn } from "./options";
export function describeLevel(level: Level) {
let bricks = 0,
@ -99,10 +107,13 @@ export function max_levels(gameState: GameState) {
return 7 + gameState.perks.extra_levels;
}
export function upgradeLevelAndMaxDisplay(upgrade: Upgrade, gameState: GameState) {
export function upgradeLevelAndMaxDisplay(
upgrade: Upgrade,
gameState: GameState,
) {
const lvl = gameState.perks[upgrade.id];
const max = upgrade.max + gameState.perks.limitless
return `<span class="level ${lvl < max ? 'can-upgrade' : 'capped'}"><span>${lvl}</span><span>${max}</span></span>`
const max = upgrade.max + gameState.perks.limitless;
return `<span class="level ${lvl < max ? "can-upgrade" : "capped"}"><span>${lvl}</span><span>${max}</span></span>`;
}
export function pickedUpgradesHTMl(gameState: GameState) {
@ -111,7 +122,6 @@ export function pickedUpgradesHTMl(gameState: GameState) {
.map((u) => {
const newMax = Math.max(0, u.max + gameState.perks.limitless);
const state = (gameState.perks[u.id] && 1) || (!newMax && 2) || 3;
return {
state,
@ -320,6 +330,10 @@ export function hoursSpentPlaying() {
}
}
export function escapeAttribute(str:String){
return str.replace(/&/gi,'&amp;').replace(/</gi,'&lt;').replace(/"/gi,'&quot;').replace(/'/gi,'&#39;')
}
export function escapeAttribute(str: String) {
return str
.replace(/&/gi, "&amp;")
.replace(/</gi, "&lt;")
.replace(/"/gi, "&quot;")
.replace(/'/gi, "&#39;");
}

View file

@ -71,6 +71,16 @@
"level_up.go": "",
"level_up.instructions": "",
"level_up.maxed_upgrade": "",
"level_up.missed.best": "",
"level_up.missed.catchRate.best": "",
"level_up.missed.catchRate.good": "",
"level_up.missed.good": "",
"level_up.missed.levelMisses.best": "",
"level_up.missed.levelMisses.good": "",
"level_up.missed.levelTime.best": "",
"level_up.missed.levelTime.good": "",
"level_up.missed.levelWallBounces.best": "",
"level_up.missed.levelWallBounces.good": "",
"level_up.no_points": "",
"level_up.pick": "",
"level_up.pick_upgrade": "",

View file

@ -2597,6 +2597,381 @@
</translation>
</translations>
</concept_node>
<folder_node>
<name>missed</name>
<children>
<concept_node>
<name>best</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<folder_node>
<name>catchRate</name>
<children>
<concept_node>
<name>best</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>good</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<concept_node>
<name>good</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<folder_node>
<name>levelMisses</name>
<children>
<concept_node>
<name>best</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>good</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>levelTime</name>
<children>
<concept_node>
<name>best</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>good</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>levelWallBounces</name>
<children>
<concept_node>
<name>best</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>good</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<concept_node>
<name>no_points</name>
<description/>

View file

@ -71,6 +71,16 @@
"level_up.go": "",
"level_up.instructions": "",
"level_up.maxed_upgrade": "",
"level_up.missed.best": "",
"level_up.missed.catchRate.best": "",
"level_up.missed.catchRate.good": "",
"level_up.missed.good": "",
"level_up.missed.levelMisses.best": "",
"level_up.missed.levelMisses.good": "",
"level_up.missed.levelTime.best": "",
"level_up.missed.levelTime.good": "",
"level_up.missed.levelWallBounces.best": "",
"level_up.missed.levelWallBounces.good": "",
"level_up.no_points": "",
"level_up.pick": "",
"level_up.pick_upgrade": "",

View file

@ -69,8 +69,18 @@
"lab.select_level": "Select a level to play on",
"lab.unlocks_at": "Unlocks at total score {{score}}",
"level_up.go": "Continue to level \"{{name}}\"",
"level_up.instructions": "You gained {{gain}} extra lives. You can use your {{count}} extra lives to buy upgrades below, or keep them to be safe. ",
"level_up.instructions": "You can use your {{count}} lives to buy upgrades below or keep them to be safe. ",
"level_up.maxed_upgrade": "\"{{name}}\" is at max level",
"level_up.missed.best": "Expert challenge",
"level_up.missed.catchRate.best": "Catch {{target}}% of coins to gain one more choice.",
"level_up.missed.catchRate.good": "Catch {{target}}% of coins to gain one more life and choice.",
"level_up.missed.good": "Enthusiast challenge",
"level_up.missed.levelMisses.best": "Miss the bricks less than {{target}} times to gain one more choice.",
"level_up.missed.levelMisses.good": "Miss the bricks less than {{target}} times to gain one more life and choice.",
"level_up.missed.levelTime.best": "Clear the level in less than {{target}} seconds to gain one more choice.",
"level_up.missed.levelTime.good": "Clear the level in less than {{target}} seconds to gain one more life and choice.",
"level_up.missed.levelWallBounces.best": "Hit the walls less than {{target}} times to gain one more choice.",
"level_up.missed.levelWallBounces.good": "Hit the walls less than {{target}} times to gain one more life and choice.",
"level_up.no_points": "You've spent all your extra lives.",
"level_up.pick": "Pick",
"level_up.pick_upgrade": "Get \"{{name}}\"",
@ -433,8 +443,8 @@
"upgrades.trampoline.tooltip": "More coins if you bounce on bricks and the paddle only",
"upgrades.trampoline.verbose_description": "+{{lvl}} combo per paddle bounce,-{{lvl}} combo per bounce on any border. One of the rare combo upgrades that don't add a reset condition",
"upgrades.transparency.name": "Transparency",
"upgrades.transparency.tooltip": "+50% coins but the ball is sometimes invisible",
"upgrades.transparency.verbose_description": "Ball becomes transparent at the top of the screen.\n +{{percent}} % coins when all balls are at full transparency. \nHigher levels make the ball transparent sooner and increase the point bonus.",
"upgrades.transparency.tooltip": "+50% coins. The ball is invisible at the top of the screen.",
"upgrades.transparency.verbose_description": "Ball becomes transparent at the top of the screen.\n +{{percent}} % coins. \nHigher levels make the ball transparent sooner and increase the point bonus.",
"upgrades.trickledown.name": "Trickle down",
"upgrades.trickledown.tooltip": "Coins appear at the top of the screen.",
"upgrades.trickledown.verbose_description": "The coins might sit on top of a brick if there are bricks on the top row, in that case they will fall down after you break that brick. ",

View file

@ -71,6 +71,16 @@
"level_up.go": "",
"level_up.instructions": "",
"level_up.maxed_upgrade": "",
"level_up.missed.best": "",
"level_up.missed.catchRate.best": "",
"level_up.missed.catchRate.good": "",
"level_up.missed.good": "",
"level_up.missed.levelMisses.best": "",
"level_up.missed.levelMisses.good": "",
"level_up.missed.levelTime.best": "",
"level_up.missed.levelTime.good": "",
"level_up.missed.levelWallBounces.best": "",
"level_up.missed.levelWallBounces.good": "",
"level_up.no_points": "",
"level_up.pick": "",
"level_up.pick_upgrade": "",

View file

@ -71,6 +71,16 @@
"level_up.go": "",
"level_up.instructions": "",
"level_up.maxed_upgrade": "",
"level_up.missed.best": "",
"level_up.missed.catchRate.best": "",
"level_up.missed.catchRate.good": "",
"level_up.missed.good": "",
"level_up.missed.levelMisses.best": "",
"level_up.missed.levelMisses.good": "",
"level_up.missed.levelTime.best": "",
"level_up.missed.levelTime.good": "",
"level_up.missed.levelWallBounces.best": "",
"level_up.missed.levelWallBounces.good": "",
"level_up.no_points": "",
"level_up.pick": "",
"level_up.pick_upgrade": "",

View file

@ -71,6 +71,16 @@
"level_up.go": "",
"level_up.instructions": "",
"level_up.maxed_upgrade": "",
"level_up.missed.best": "",
"level_up.missed.catchRate.best": "",
"level_up.missed.catchRate.good": "",
"level_up.missed.good": "",
"level_up.missed.levelMisses.best": "",
"level_up.missed.levelMisses.good": "",
"level_up.missed.levelTime.best": "",
"level_up.missed.levelTime.good": "",
"level_up.missed.levelWallBounces.best": "",
"level_up.missed.levelWallBounces.good": "",
"level_up.no_points": "",
"level_up.pick": "",
"level_up.pick_upgrade": "",

View file

@ -71,6 +71,16 @@
"level_up.go": "",
"level_up.instructions": "",
"level_up.maxed_upgrade": "",
"level_up.missed.best": "",
"level_up.missed.catchRate.best": "",
"level_up.missed.catchRate.good": "",
"level_up.missed.good": "",
"level_up.missed.levelMisses.best": "",
"level_up.missed.levelMisses.good": "",
"level_up.missed.levelTime.best": "",
"level_up.missed.levelTime.good": "",
"level_up.missed.levelWallBounces.best": "",
"level_up.missed.levelWallBounces.good": "",
"level_up.no_points": "",
"level_up.pick": "",
"level_up.pick_upgrade": "",

View file

@ -17,7 +17,7 @@ import {
isLevelLocked,
reasonLevelIsLocked,
} from "./get_level_unlock_condition";
import {dontOfferTooSoon} from "./openUpgradesPicker";
import { dontOfferTooSoon } from "./openUpgradesPicker";
export function getRunLevels(
params: RunParams,

View file

@ -1,13 +1,17 @@
import {GameState} from "./types";
import {asyncAlert} from "./asyncAlert";
import {t} from "./i18n/i18n";
import {levelsListHTMl, max_levels, pickedUpgradesHTMl} from "./game_utils";
import {getCreativeModeWarning, getHistory} from "./gameOver";
import {pause} from "./game";
import {allLevels, icons} from "./loadGameData";
import {firstWhere} from "./pure_functions";
import {getSettingValue, getTotalScore} from "./settings";
import {getLevelUnlockCondition, reasonLevelIsLocked, upgradeName,} from "./get_level_unlock_condition";
import { GameState } from "./types";
import { asyncAlert } from "./asyncAlert";
import { t } from "./i18n/i18n";
import { levelsListHTMl, max_levels, pickedUpgradesHTMl } from "./game_utils";
import { getCreativeModeWarning, getHistory } from "./gameOver";
import { pause } from "./game";
import { allLevels, icons } from "./loadGameData";
import { firstWhere } from "./pure_functions";
import { getSettingValue, getTotalScore } from "./settings";
import {
getLevelUnlockCondition,
reasonLevelIsLocked,
upgradeName,
} from "./get_level_unlock_condition";
export async function openScorePanel(gameState: GameState) {
pause(true);
@ -96,4 +100,3 @@ export function getNearestUnlockHTML(gameState: GameState) {
`;
}

View file

@ -1,147 +1,223 @@
import {GameState, PerkId} from "./types";
import { GameState, PerkId } from "./types";
import {
catchRateBest,
catchRateGood,
levelTimeBest,
levelTimeGood,
missesBest,
missesGood,
wallBouncedBest,
wallBouncedGood
catchRateBest,
catchRateGood,
levelTimeBest,
levelTimeGood,
missesBest,
missesGood,
wallBouncedBest,
wallBouncedGood,
} from "./pure_functions";
import {t} from "./i18n/i18n";
import {icons, upgrades} from "./loadGameData";
import {asyncAlert} from "./asyncAlert";
import { t } from "./i18n/i18n";
import { icons, upgrades } from "./loadGameData";
import { asyncAlert } from "./asyncAlert";
import {
escapeAttribute,
getPossibleUpgrades,
levelsListHTMl,
max_levels,
upgradeLevelAndMaxDisplay
escapeAttribute,
getPossibleUpgrades,
levelsListHTMl,
max_levels,
upgradeLevelAndMaxDisplay,
} from "./game_utils";
import {getNearestUnlockHTML} from "./openScorePanel";
import { getNearestUnlockHTML } from "./openScorePanel";
export async function openUpgradesPicker(gameState: GameState) {
const catchRate =
gameState.levelCoughtCoins / (gameState.levelSpawnedCoins || 1);
const catchRate =
gameState.levelCoughtCoins / (gameState.levelSpawnedCoins || 1);
let choices = 3
let livesWon=1
let choices = 3;
let livesWon = 1;
let missedOpportunities = [];
const good = ""; // '<strong>'+t('level_up.missed.good')+'</strong>: '
const best = ""; //'<strong>'+t('level_up.missed.best')+'</strong>: '
if (gameState.levelWallBounces < wallBouncedGood) {
choices++;
if (gameState.levelWallBounces < wallBouncedGood) {
choices++;
livesWon++;
} else {
missedOpportunities.push(
good +
t("level_up.missed.levelWallBounces.good", {
target: wallBouncedGood,
}),
);
}
if (gameState.levelWallBounces < wallBouncedBest) {
choices++;
} else {
missedOpportunities.push(
best +
t("level_up.missed.levelWallBounces.best", {
target: wallBouncedBest,
}),
);
}
livesWon++;
if (gameState.levelWallBounces < wallBouncedBest) {
choices++;
}
}
if (gameState.levelTime < levelTimeGood * 1000) {
choices++;
livesWon++;
} else {
missedOpportunities.push(
good +
t("level_up.missed.levelTime.good", {
target: levelTimeGood,
}),
);
}
if (gameState.levelTime < levelTimeBest * 1000) {
choices++;
} else {
missedOpportunities.push(
best +
t("level_up.missed.levelTime.best", {
target: levelTimeBest,
}),
);
}
if (gameState.levelTime < levelTimeGood * 1000) {
choices++;
livesWon++;
if (gameState.levelTime < levelTimeBest * 1000) {
choices++;
}
}
if (catchRate > catchRateGood / 100) {
choices++;
livesWon++;
if (catchRate > catchRateBest / 100) {
choices++;
}
}
if (gameState.levelMisses < missesGood) {
choices++;
livesWon++;
if (gameState.levelMisses < missesBest) {
choices++;
}
}
if (catchRate > catchRateGood / 100) {
choices++;
livesWon++;
} else {
missedOpportunities.push(
good +
t("level_up.missed.catchRate.good", {
target: catchRateGood,
}),
);
}
if (catchRate > catchRateBest / 100) {
choices++;
} else {
missedOpportunities.push(
best +
t("level_up.missed.catchRate.best", {
target: catchRateBest,
}),
);
}
gameState.extra_lives+=livesWon
if (gameState.levelMisses < missesGood) {
choices++;
livesWon++;
} else {
missedOpportunities.push(
good +
t("level_up.missed.levelMisses.good", {
target: missesGood,
}),
);
}
if (gameState.levelMisses < missesBest) {
choices++;
} else {
missedOpportunities.push(
best +
t("level_up.missed.levelMisses.best", {
target: missesBest,
}),
);
}
let offered: PerkId[] = getPossibleUpgrades(gameState)
.map((u) => ({
...u,
score: Math.random() + (gameState.lastOffered[u.id] || 0),
}))
.sort((a, b) => a.score - b.score)
.filter((u) => gameState.perks[u.id] < u.max + gameState.perks.limitless)
.map(u => u.id)
gameState.extra_lives += livesWon;
const fromStart = upgrades.map(u => u.id).filter(id => gameState.perks[id])
let offered: PerkId[] = getPossibleUpgrades(gameState)
.map((u) => ({
...u,
score: Math.random() + (gameState.lastOffered[u.id] || 0),
}))
.sort((a, b) => a.score - b.score)
.filter((u) => gameState.perks[u.id] < u.max + gameState.perks.limitless)
.map((u) => u.id);
while (true) {
const fromStart = upgrades
.map((u) => u.id)
.filter((id) => gameState.perks[id]);
const updatedChoices = gameState.perks.one_more_choice + choices
let list = upgrades.filter(u => offered.slice(0, updatedChoices).includes(u.id) || gameState.perks[u.id])
while (true) {
const updatedChoices = gameState.perks.one_more_choice + choices;
let list = upgrades.filter(
(u) =>
offered.slice(0, updatedChoices).includes(u.id) ||
gameState.perks[u.id],
);
list = list.filter(u => fromStart.includes(u.id))
.concat(list.filter(u => !fromStart.includes(u.id)))
list = list
.filter((u) => fromStart.includes(u.id))
.concat(list.filter((u) => !fromStart.includes(u.id)));
list.forEach((u) => {
dontOfferTooSoon(gameState, u.id);
});
list.forEach((u) => {
dontOfferTooSoon(gameState, u.id);
});
const upgradeId = await asyncAlert<PerkId | null>({
title: t("level_up.title", {
level: gameState.currentLevel,
max: max_levels(gameState),
}),
content: [
{
text: t("level_up.go", { name: gameState.level.name }),
icon: icons[gameState.level.name],
value: null,
},
const upgradeId = await asyncAlert<PerkId | null>({
title:
t("level_up.title", {
level: gameState.currentLevel,
max: max_levels(gameState),
}),
content: [
{
text: t('level_up.go', {name: gameState.level.name}),
icon: icons[gameState.level.name],
value: null,
},
gameState.extra_lives
? `<p>${t("level_up.instructions", {
count: gameState.extra_lives,
gain: livesWon,
})}</p>`
: `<p>${t("level_up.no_points")}</p>`,
...list.map((u) => {
const max = u.max + gameState.perks.limitless;
const lvl = gameState.perks[u.id];
gameState.extra_lives ? `<p>${t("level_up.instructions", {
count: gameState.extra_lives,
gain:livesWon
})}</p>` : `<p>${t("level_up.no_points")}</p>`,
...list.map((u) => {
const max = u.max + gameState.perks.limitless
const lvl = gameState.perks[u.id]
const button =
!gameState.extra_lives || gameState.perks[u.id] >= max
? ""
: ` <button data-resolve-to="${u.id}">${
lvl ? t("level_up.upgrade") : t("level_up.pick")
}</button>`;
const button = !gameState.extra_lives || gameState.perks[u.id] >= max ?
'' : ` <button data-resolve-to="${u.id}">${
lvl ? t('level_up.upgrade') : t('level_up.pick')
}</button>`
const lvlInfo = lvl ? upgradeLevelAndMaxDisplay(u, gameState) : ''
return `<div class="upgrade choice ${
(!lvl && gameState.extra_lives && 'free') ||
(lvl && 'used') ||
'greyed-out'
}" >
const lvlInfo = lvl ? upgradeLevelAndMaxDisplay(u, gameState) : "";
return `<div class="upgrade choice ${
(!lvl && gameState.extra_lives && "free") ||
(lvl && "used") ||
"greyed-out"
}" >
${icons["icon:" + u.id]}
<p data-tooltip="${escapeAttribute(u.fullHelp(Math.max(1, lvl)))}">
<strong>${u.name}</strong> ${lvlInfo}
${u.help(Math.max(1, lvl))}
</p>
${button}
</div>`
})
,
levelsListHTMl(gameState, gameState.currentLevel),
getNearestUnlockHTML(gameState),
`<div id="level-recording-container"></div>`,
],
});
</div>`;
}),
...missedOpportunities.map(
(reason) =>
`<div class="upgrade choice greyed-out" >
${icons["icon:locked"]}
<p>
${reason}
</p>
</div>`,
),
levelsListHTMl(gameState, gameState.currentLevel),
getNearestUnlockHTML(gameState),
`<div id="level-recording-container"></div>`,
],
});
if (upgradeId) {
gameState.perks[upgradeId]++;
gameState.runStatistics.upgrades_picked++;
gameState.extra_lives--
} else {
return
}
if (upgradeId) {
gameState.perks[upgradeId]++;
gameState.runStatistics.upgrades_picked++;
gameState.extra_lives--;
} else {
return;
}
}
}
export function dontOfferTooSoon(gameState: GameState, id: PerkId) {
gameState.lastOffered[id] = Math.round(Date.now() / 1000);
}
gameState.lastOffered[id] = Math.round(Date.now() / 1000);
}

View file

@ -20,17 +20,11 @@ export function ballTransparency(ball: Ball, gameState: GameState) {
export function coinsBoostedCombo(gameState: GameState) {
let boost =
1 + gameState.perks.sturdy_bricks / 2 + gameState.perks.smaller_puck / 2;
if (gameState.perks.transparency) {
let min = 1;
gameState.balls.forEach((ball) => {
const bt = ballTransparency(ball, gameState);
if (bt < min) {
min = bt;
}
});
boost += (min * gameState.perks.transparency) / 2;
}
1 +
gameState.perks.sturdy_bricks / 2 +
gameState.perks.smaller_puck / 2 +
gameState.perks.transparency / 2;
if (gameState.perks.minefield) {
gameState.bricks.forEach((brick) => {
if (brick === "black") {
@ -101,13 +95,13 @@ export function firstWhere<Input, Output>(
}
}
export const wallBouncedBest = 3,
wallBouncedGood = 10,
levelTimeBest = 30,
levelTimeGood = 60,
catchRateBest = 95,
export const wallBouncedBest = 2,
wallBouncedGood = 7,
levelTimeBest = 10,
levelTimeGood = 45,
catchRateBest = 99,
catchRateGood = 90,
missesBest = 3,
missesBest = 1,
missesGood = 6;
export const MAX_LEVEL_SIZE = 21;

View file

@ -108,8 +108,12 @@ export function render(gameState: GameState) {
: "") +
`<span class="score" data-tooltip="${t("play.score_tooltip")}">$${gameState.score}</span>`;
scoreDisplay.classList[gameState.startParams.computer_controlled ? 'add':'remove']('computer_controlled');
scoreDisplay.classList[gameState.lastScoreIncrease > gameState.levelTime - 500 ? 'add':'remove']('active');
scoreDisplay.classList[
gameState.startParams.computer_controlled ? "add" : "remove"
]("computer_controlled");
scoreDisplay.classList[
gameState.lastScoreIncrease > gameState.levelTime - 500 ? "add" : "remove"
]("active");
// Clear
if (!isOptionOn("basic") && level.svg && level.color === "#000000") {

View file

@ -13,11 +13,10 @@ export function hideAnyTooltip() {
tooltip.style.display = "none";
}
function setupMobileTooltips(tooltip: HTMLDivElement) {
tooltip.className = "mobile";
function openTooltip(e: Event) {
console.log('openTooltip',e)
console.log("openTooltip", e);
hideAnyTooltip();
const hovering = e.target as HTMLElement;
if (!hovering?.hasAttribute("data-help-content")) {
@ -33,7 +32,6 @@ function setupMobileTooltips(tooltip: HTMLDivElement) {
document.body.addEventListener("click", openTooltip, true);
document.addEventListener("scroll", hideAnyTooltip);
}
function setupDesktopTooltips(tooltip: HTMLDivElement) {

View file

@ -914,7 +914,10 @@ export const rawUpgrades = [
max: 4,
name: t("upgrades.passive_income.name"),
help: (lvl: number) =>
t("upgrades.passive_income.tooltip", { time: (lvl * 0.1 - 0.05).toFixed(2), lvl }),
t("upgrades.passive_income.tooltip", {
time: (lvl * 0.1 - 0.05).toFixed(2),
lvl,
}),
fullHelp: (lvl: number) =>
t("upgrades.passive_income.verbose_description", {
time: (lvl * 0.1 - 0.05).toFixed(2),