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 # Changelog
## To do ## To do
- instead of bouncing the ball,loosing a life pauses the game (with coins still in the air)
- rewoks perks choices ## Done
- goal : limit perk fatigue, avoid wall of texts, clarify challenges, allow users to skip - rewoked perks choices to limit perk fatigue, avoid wall of texts, allow users to skip
- remove rerolls - removed rerolls
- offer to pick 1 upgrade out of 3 choices - offer to pick 1 upgrade out of 3 choices
- playing well adds 1 upgrade and 1 choice - playing well adds 1 upgrade and 1 choice
- playing even better adds 1 choice - playing even better adds 1 choice
- "more choices" perk adds 1 choice - "more choices" perk adds 1 choice
- you can skip the upgrades and they'll be saved for later - 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 - 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). - 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 - 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 - Fix : click tooltip to open on mobile, click anywhere to close
- Can't press help buttons in Creative Menu - Can't press help buttons in Creative Menu

View file

@ -29,8 +29,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29097764 versionCode = 29099215
versionName = "29097764" versionName = "29099215"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true 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. // The version of the cache.
const VERSION = "29097764"; const VERSION = "29099215";
// The name of the cache // The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`; const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -101,8 +101,8 @@ export async function asyncAlert<t>({
popup.appendChild(addto); popup.appendChild(addto);
} }
const buttonWrap = document.createElement("div") const buttonWrap = document.createElement("div");
addto.appendChild(buttonWrap) addto.appendChild(buttonWrap);
const { const {
text, text,
@ -136,7 +136,9 @@ ${icon}
} }
button.className = button.className =
className + (lastClickedItemIndex === index ? " needs-focus" : "")+' choice-button'; className +
(lastClickedItemIndex === index ? " needs-focus" : "") +
" choice-button";
buttonWrap.appendChild(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\"" "credit": "Suggested by Big Goober. The red ghost, Blinky, from the arcade game \"Pac Man\""
}, },
{ {
"color": "#000000", "name": "Fish",
"size": 11, "size": 11,
"bricks": "______________________________________________bbbb______tttttt___btgttbttt_bbtttttbtttb___ttbttt_bb_tttttt___b___________", "bricks": "______________________________________________bbbb______tttttt___btgttbttt_bbtttttbtttb___ttbttt_bb_tttttt___b___________",
"name": "Fish",
"credit": "A fish based on the fish discord emoji. Suggested by Big Goober. " "credit": "A fish based on the fish discord emoji. Suggested by Big Goober. "
}, },
{ {
"color": "#115988", "name": "Spider",
"size": 7, "size": 7,
"bricks": "_l_____Sgg____ggSgBB_gSgBBBBSgggggg_gg___g_g_g_g_", "bricks": "_l_____Sgg____ggSgBB_gSgBBBBSgggggg_gg___g_g_g_g_",
"name": "Spider",
"credit": "Suggested by obigre." "credit": "Suggested by obigre."
}, },
{ {
"color": "#115988", "name": "Gliders",
"size": 8, "size": 8,
"bricks": "g_g______gg___l__g__l_l______ll__c________cc__W__cc____W_____WWW", "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" "credit": "iSuggested by obigre. Inspired by Conway's gliders"
}, },
{ {
"color": "#000000", "name": "Lone island",
"size": 8, "size": 8,
"bricks": "C__________kkk____kkOkk___kkkO_k_k_k_O_______O______CC__tttCCCCt", "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 ?" "credit": "Suggested by obigre. Which game would you take there ?"
}, },
{ {
"color": "#000000", "name": "Spacewyrm Jon",
"size": 8, "size": 8,
"bricks": "___PPP____PPPP____SSSP____WPWP_P__PPP_PP___PP_____yPPy__bWWyyWWb", "bricks": "___PPP____PPPP____SSSP____WPWP_P__PPP_PP___PP_____yPPy__bWWyyWWb",
"name": "Spacewyrm Jon",
"credit": "Suggested by obigre. The invertebrate hero with a gun" "credit": "Suggested by obigre. The invertebrate hero with a gun"
}, },
{ {
"color": "#115988", "name": "Taijitu",
"size": 7, "size": 7,
"bricks": "_WWWWW_W__WWWWgg__WBWggg_WWWgBg__WWgggg__g_ggggg_", "bricks": "_WWWWW_W__WWWWgg__WBWggg_WWWgBg__WWgggg__g_ggggg_",
"name": "Taijitu",
"credit": "Suggested by obigre. Yin and yang fishes" "credit": "Suggested by obigre. Yin and yang fishes"
}, },
{ {
"color": "#115988", "name": "Egg pan",
"size": 5, "size": 5,
"bricks": "WWWWgWWyWggWWWggggg____g_", "bricks": "WWWWgWWyWggWWWggggg____g_",
"name": "Egg pan",
"credit": "Suggested by obigre. Fried and tasty" "credit": "Suggested by obigre. Fried and tasty"
}, },
{ {
"color": "#000000", "name": "Inception",
"size": 20, "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____________________________________________", "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" "credit": "Breakout 71 within Breakout 71. By Noodlemire"
}, },
{ {
"color": "#000000", "name": "Chess",
"size": 21, "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_", "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" "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"], "required": ["respawn", "wrap_left", "sapper"],
"forbidden": ["shocks", "metamorphosis", "pierce"], "forbidden": ["shocks", "metamorphosis", "pierce"],
"minScore": 14200 "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, font-family:
Courier, Courier New,
Lucida Sans Typewriter, Courier,
Lucida Typewriter, Lucida Sans Typewriter,
monospace; Lucida Typewriter,
monospace;
box-sizing: border-box; box-sizing: border-box;
} }
@ -73,7 +74,6 @@ canvas:not(#game) {
line-height: 20px; line-height: 20px;
box-shadow: 0 2px #fff; box-shadow: 0 2px #fff;
} }
} }
#score { #score {
@ -210,7 +210,6 @@ body:not(.has-alert-open) #popup {
opacity: 0.2; opacity: 0.2;
} }
} }
} }
> button[data-help-content] { > button[data-help-content] {
@ -425,7 +424,7 @@ h2.histogram-title strong {
.level { .level {
color: #000; color: #000;
background: #FFF; background: #fff;
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
font-size: 12px; font-size: 12px;
@ -435,7 +434,7 @@ h2.histogram-title strong {
position: relative; position: relative;
//top: -3px; //top: -3px;
font-weight: bold; font-weight: bold;
border: 1px solid #FFF; border: 1px solid #fff;
//margin-left: 5px; //margin-left: 5px;
> span { > span {
@ -443,38 +442,34 @@ h2.histogram-title strong {
position: relative; position: relative;
&:first-child { &:first-child {
padding: 3px 6px 0 2px; padding: 3px 6px 0 2px;
color: #000; color: #000;
background: #FFF; background: #fff;
} }
&:last-child { &:last-child {
padding: 3px 3px 0 2px; padding: 3px 3px 0 2px;
color: #FFF; color: #fff;
background: #000; background: #000;
&:before { &:before {
content: ""; content: "";
display: block; display: block;
background: black;; background: black;
position: absolute; position: absolute;
left: -2px; left: -2px;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 4px; width: 4px;
transform: skewX(-10deg) transform: skewX(-10deg);
} }
} }
} }
&.capped { &.capped {
> span:first-child { > span:first-child {
color: #fff;
color: #FFF;
background: #000; background: #000;
} }
> span:last-child::before { > span:last-child::before {
@ -511,16 +506,22 @@ h2.histogram-title strong {
font-weight: bold; font-weight: bold;
padding: 5px; padding: 5px;
border-radius: 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; border: 2px solid white;
cursor: pointer; cursor: pointer;
text-shadow: 0 0 4px rgba(0, 0, 0, 0.3); text-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
user-select: none; user-select: none;
&:active{ &:active {
transform: translate(0,4px); transform: translate(0, 4px);
box-shadow: 0 0px 0 gold, 0 0px 10px black; 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; border-radius: 2px;
padding-right: 10px; padding-right: 10px;
pointer-events: none; pointer-events: none;
transition: opacity 200ms, transition:
transform 200ms; opacity 200ms,
transform 200ms;
z-index: 7; z-index: 7;
&.hidden { &.hidden {

View file

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

View file

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

View file

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

View file

@ -2597,6 +2597,381 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>no_points</name> <name>no_points</name>
<description/> <description/>

View file

@ -71,6 +71,16 @@
"level_up.go": "", "level_up.go": "",
"level_up.instructions": "", "level_up.instructions": "",
"level_up.maxed_upgrade": "", "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.no_points": "",
"level_up.pick": "", "level_up.pick": "",
"level_up.pick_upgrade": "", "level_up.pick_upgrade": "",

View file

@ -69,8 +69,18 @@
"lab.select_level": "Select a level to play on", "lab.select_level": "Select a level to play on",
"lab.unlocks_at": "Unlocks at total score {{score}}", "lab.unlocks_at": "Unlocks at total score {{score}}",
"level_up.go": "Continue to level \"{{name}}\"", "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.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.no_points": "You've spent all your extra lives.",
"level_up.pick": "Pick", "level_up.pick": "Pick",
"level_up.pick_upgrade": "Get \"{{name}}\"", "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.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.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.name": "Transparency",
"upgrades.transparency.tooltip": "+50% coins but the ball is sometimes invisible", "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 when all balls are at full transparency. \nHigher levels make the ball transparent sooner and increase the point bonus.", "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.name": "Trickle down",
"upgrades.trickledown.tooltip": "Coins appear at the top of the screen.", "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. ", "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.go": "",
"level_up.instructions": "", "level_up.instructions": "",
"level_up.maxed_upgrade": "", "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.no_points": "",
"level_up.pick": "", "level_up.pick": "",
"level_up.pick_upgrade": "", "level_up.pick_upgrade": "",

View file

@ -71,6 +71,16 @@
"level_up.go": "", "level_up.go": "",
"level_up.instructions": "", "level_up.instructions": "",
"level_up.maxed_upgrade": "", "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.no_points": "",
"level_up.pick": "", "level_up.pick": "",
"level_up.pick_upgrade": "", "level_up.pick_upgrade": "",

View file

@ -71,6 +71,16 @@
"level_up.go": "", "level_up.go": "",
"level_up.instructions": "", "level_up.instructions": "",
"level_up.maxed_upgrade": "", "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.no_points": "",
"level_up.pick": "", "level_up.pick": "",
"level_up.pick_upgrade": "", "level_up.pick_upgrade": "",

View file

@ -71,6 +71,16 @@
"level_up.go": "", "level_up.go": "",
"level_up.instructions": "", "level_up.instructions": "",
"level_up.maxed_upgrade": "", "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.no_points": "",
"level_up.pick": "", "level_up.pick": "",
"level_up.pick_upgrade": "", "level_up.pick_upgrade": "",

View file

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

View file

@ -1,13 +1,17 @@
import {GameState} from "./types"; import { GameState } from "./types";
import {asyncAlert} from "./asyncAlert"; import { asyncAlert } from "./asyncAlert";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {levelsListHTMl, max_levels, pickedUpgradesHTMl} from "./game_utils"; import { levelsListHTMl, max_levels, pickedUpgradesHTMl } from "./game_utils";
import {getCreativeModeWarning, getHistory} from "./gameOver"; import { getCreativeModeWarning, getHistory } from "./gameOver";
import {pause} from "./game"; import { pause } from "./game";
import {allLevels, icons} from "./loadGameData"; import { allLevels, icons } from "./loadGameData";
import {firstWhere} from "./pure_functions"; import { firstWhere } from "./pure_functions";
import {getSettingValue, getTotalScore} from "./settings"; import { getSettingValue, getTotalScore } from "./settings";
import {getLevelUnlockCondition, reasonLevelIsLocked, upgradeName,} from "./get_level_unlock_condition"; import {
getLevelUnlockCondition,
reasonLevelIsLocked,
upgradeName,
} from "./get_level_unlock_condition";
export async function openScorePanel(gameState: GameState) { export async function openScorePanel(gameState: GameState) {
pause(true); 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 { import {
catchRateBest, catchRateBest,
catchRateGood, catchRateGood,
levelTimeBest, levelTimeBest,
levelTimeGood, levelTimeGood,
missesBest, missesBest,
missesGood, missesGood,
wallBouncedBest, wallBouncedBest,
wallBouncedGood wallBouncedGood,
} from "./pure_functions"; } from "./pure_functions";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {icons, upgrades} from "./loadGameData"; import { icons, upgrades } from "./loadGameData";
import {asyncAlert} from "./asyncAlert"; import { asyncAlert } from "./asyncAlert";
import { import {
escapeAttribute, escapeAttribute,
getPossibleUpgrades, getPossibleUpgrades,
levelsListHTMl, levelsListHTMl,
max_levels, max_levels,
upgradeLevelAndMaxDisplay upgradeLevelAndMaxDisplay,
} from "./game_utils"; } from "./game_utils";
import {getNearestUnlockHTML} from "./openScorePanel"; import { getNearestUnlockHTML } from "./openScorePanel";
export async function openUpgradesPicker(gameState: GameState) { export async function openUpgradesPicker(gameState: GameState) {
const catchRate = const catchRate =
gameState.levelCoughtCoins / (gameState.levelSpawnedCoins || 1); gameState.levelCoughtCoins / (gameState.levelSpawnedCoins || 1);
let choices = 3 let choices = 3;
let livesWon=1 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) { if (gameState.levelWallBounces < wallBouncedGood) {
choices++; 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.levelTime < levelTimeGood * 1000) {
if (gameState.levelWallBounces < wallBouncedBest) { choices++;
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) { if (catchRate > catchRateGood / 100) {
choices++; choices++;
livesWon++; livesWon++;
if (gameState.levelTime < levelTimeBest * 1000) { } else {
choices++; missedOpportunities.push(
} good +
} t("level_up.missed.catchRate.good", {
if (catchRate > catchRateGood / 100) { target: catchRateGood,
choices++; }),
livesWon++; );
if (catchRate > catchRateBest / 100) { }
choices++; if (catchRate > catchRateBest / 100) {
} choices++;
} } else {
if (gameState.levelMisses < missesGood) { missedOpportunities.push(
choices++; best +
livesWon++; t("level_up.missed.catchRate.best", {
if (gameState.levelMisses < missesBest) { target: catchRateBest,
choices++; }),
} );
} }
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) gameState.extra_lives += livesWon;
.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)
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 while (true) {
let list = upgrades.filter(u => offered.slice(0, updatedChoices).includes(u.id) || gameState.perks[u.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],
);
list = list.filter(u => fromStart.includes(u.id)) list = list
.concat(list.filter(u => !fromStart.includes(u.id))) .filter((u) => fromStart.includes(u.id))
.concat(list.filter((u) => !fromStart.includes(u.id)));
list.forEach((u) => { list.forEach((u) => {
dontOfferTooSoon(gameState, u.id); 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>({ gameState.extra_lives
title: ? `<p>${t("level_up.instructions", {
t("level_up.title", { count: gameState.extra_lives,
level: gameState.currentLevel, gain: livesWon,
max: max_levels(gameState), })}</p>`
}), : `<p>${t("level_up.no_points")}</p>`,
content: [ ...list.map((u) => {
{ const max = u.max + gameState.perks.limitless;
text: t('level_up.go', {name: gameState.level.name}), const lvl = gameState.perks[u.id];
icon: icons[gameState.level.name],
value: null,
},
gameState.extra_lives ? `<p>${t("level_up.instructions", { const button =
count: gameState.extra_lives, !gameState.extra_lives || gameState.perks[u.id] >= max
gain:livesWon ? ""
})}</p>` : `<p>${t("level_up.no_points")}</p>`, : ` <button data-resolve-to="${u.id}">${
...list.map((u) => { lvl ? t("level_up.upgrade") : t("level_up.pick")
const max = u.max + gameState.perks.limitless }</button>`;
const lvl = gameState.perks[u.id]
const button = !gameState.extra_lives || gameState.perks[u.id] >= max ? const lvlInfo = lvl ? upgradeLevelAndMaxDisplay(u, gameState) : "";
'' : ` <button data-resolve-to="${u.id}">${ return `<div class="upgrade choice ${
lvl ? t('level_up.upgrade') : t('level_up.pick') (!lvl && gameState.extra_lives && "free") ||
}</button>` (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]} ${icons["icon:" + u.id]}
<p data-tooltip="${escapeAttribute(u.fullHelp(Math.max(1, lvl)))}"> <p data-tooltip="${escapeAttribute(u.fullHelp(Math.max(1, lvl)))}">
<strong>${u.name}</strong> ${lvlInfo} <strong>${u.name}</strong> ${lvlInfo}
${u.help(Math.max(1, lvl))} ${u.help(Math.max(1, lvl))}
</p> </p>
${button} ${button}
</div>` </div>`;
}) }),
, ...missedOpportunities.map(
levelsListHTMl(gameState, gameState.currentLevel), (reason) =>
getNearestUnlockHTML(gameState), `<div class="upgrade choice greyed-out" >
`<div id="level-recording-container"></div>`, ${icons["icon:locked"]}
], <p>
}); ${reason}
</p>
</div>`,
),
levelsListHTMl(gameState, gameState.currentLevel),
getNearestUnlockHTML(gameState),
`<div id="level-recording-container"></div>`,
],
});
if (upgradeId) { if (upgradeId) {
gameState.perks[upgradeId]++; gameState.perks[upgradeId]++;
gameState.runStatistics.upgrades_picked++; gameState.runStatistics.upgrades_picked++;
gameState.extra_lives-- gameState.extra_lives--;
} else { } else {
return return;
}
} }
}
} }
export function dontOfferTooSoon(gameState: GameState, id: PerkId) { 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) { export function coinsBoostedCombo(gameState: GameState) {
let boost = let boost =
1 + gameState.perks.sturdy_bricks / 2 + gameState.perks.smaller_puck / 2; 1 +
if (gameState.perks.transparency) { gameState.perks.sturdy_bricks / 2 +
let min = 1; gameState.perks.smaller_puck / 2 +
gameState.balls.forEach((ball) => { gameState.perks.transparency / 2;
const bt = ballTransparency(ball, gameState);
if (bt < min) {
min = bt;
}
});
boost += (min * gameState.perks.transparency) / 2;
}
if (gameState.perks.minefield) { if (gameState.perks.minefield) {
gameState.bricks.forEach((brick) => { gameState.bricks.forEach((brick) => {
if (brick === "black") { if (brick === "black") {
@ -101,13 +95,13 @@ export function firstWhere<Input, Output>(
} }
} }
export const wallBouncedBest = 3, export const wallBouncedBest = 2,
wallBouncedGood = 10, wallBouncedGood = 7,
levelTimeBest = 30, levelTimeBest = 10,
levelTimeGood = 60, levelTimeGood = 45,
catchRateBest = 95, catchRateBest = 99,
catchRateGood = 90, catchRateGood = 90,
missesBest = 3, missesBest = 1,
missesGood = 6; missesGood = 6;
export const MAX_LEVEL_SIZE = 21; 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>`; `<span class="score" data-tooltip="${t("play.score_tooltip")}">$${gameState.score}</span>`;
scoreDisplay.classList[gameState.startParams.computer_controlled ? 'add':'remove']('computer_controlled'); scoreDisplay.classList[
scoreDisplay.classList[gameState.lastScoreIncrease > gameState.levelTime - 500 ? 'add':'remove']('active'); gameState.startParams.computer_controlled ? "add" : "remove"
]("computer_controlled");
scoreDisplay.classList[
gameState.lastScoreIncrease > gameState.levelTime - 500 ? "add" : "remove"
]("active");
// Clear // Clear
if (!isOptionOn("basic") && level.svg && level.color === "#000000") { if (!isOptionOn("basic") && level.svg && level.color === "#000000") {

View file

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

View file

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