Build 29095000

This commit is contained in:
Renan LE CARO 2025-04-26 22:40:32 +02:00
parent 096f7d4abd
commit 4c324d211c
27 changed files with 500 additions and 1350 deletions

View file

View file

@ -30,6 +30,7 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
## Done ## Done
- hardcoded the levels unlock conditions so that they wouldn't change at each update
- hide any tooltip on page scroll - hide any tooltip on page scroll
- added a "display level code" button in editor - added a "display level code" button in editor
- passive income : paddle transparent for a much shorter time - passive income : paddle transparent for a much shorter time
@ -573,6 +574,7 @@ Here are a few interesting games in the breakout genre :
- Wizorb : https://store.steampowered.com/app/207420/Wizorb/ - Wizorb : https://store.steampowered.com/app/207420/Wizorb/
- Ricochet infinity : https://www.myabandonware.com/game/ricochet-infinity-dxm - Ricochet infinity : https://www.myabandonware.com/game/ricochet-infinity-dxm
- Whackerball : https://store.steampowered.com/app/2192170/Whackerball/ - Whackerball : https://store.steampowered.com/app/2192170/Whackerball/
- Arkanoid Archive lists many, many more https://www.youtube.com/@ArkanoidGame
# PC game suggestions # PC game suggestions

View file

@ -29,8 +29,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29092809 versionCode = 29095000
versionName = "29092809" versionName = "29095000"
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

22
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 = "29092809"; const VERSION = "29095000";
// The name of the cache // The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`; const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -9,15 +9,11 @@ import {
restart, restart,
} from "./game"; } from "./game";
import { asyncAlert, requiredAsyncAlert } from "./asyncAlert"; import { asyncAlert, requiredAsyncAlert } from "./asyncAlert";
import { import { describeLevel, highScoreText, sumOfValues } from "./game_utils";
describeLevel,
highScoreText,
sumOfValues,
} from "./game_utils";
import { getHistory } from "./gameOver"; import { getHistory } from "./gameOver";
import { noCreative } from "./upgrades"; import { noCreative } from "./upgrades";
import { levelIconHTML } from "./levelIcon"; import { levelIconHTML } from "./levelIcon";
import {reasonLevelIsLocked} from "./get_level_unlock_condition"; import { reasonLevelIsLocked } from "./get_level_unlock_condition";
export function creativeMode(gameState: GameState) { export function creativeMode(gameState: GameState) {
return { return {
@ -46,7 +42,8 @@ export async function openCreativeModePerksPicker() {
while (true) { while (true) {
const levelOptions = [ const levelOptions = [
...allLevels.map((l, li) => { ...allLevels.map((l, li) => {
const problem = reasonLevelIsLocked(li, l.name,getHistory(), true)?.text || ""; const problem =
reasonLevelIsLocked(li, l.name, getHistory(), true)?.text || "";
return { return {
icon: icons[l.name], icon: icons[l.name],
text: l.name, text: l.name,

File diff suppressed because it is too large Load diff

View file

@ -1,38 +1,43 @@
import conditions from "./unlockConditions.json" import conditions from "./unlockConditions.json";
import levels from "./levels.json" import levels from "./levels.json";
import {rawUpgrades} from "../upgrades"; import { rawUpgrades } from "../upgrades";
import {getLevelUnlockCondition} from "../get_level_unlock_condition"; import { getLevelUnlockCondition } from "../get_level_unlock_condition";
import {UnlockCondition} from "../types"; import { UnlockCondition } from "../types";
describe("conditions", () => { describe("conditions", () => {
it("defines conditions for existing levels only", () => { it("defines conditions for existing levels only", () => {
const conditionForMissingLevel=Object.keys(conditions).filter(levelName=>!levels.find(l=>l.name===levelName)) const conditionForMissingLevel = Object.keys(conditions).filter(
expect(conditionForMissingLevel).toEqual([]); (levelName) => !levels.find((l) => l.name === levelName),
}); );
it("defines conditions with existing upgrades only", () => { expect(conditionForMissingLevel).toEqual([]);
});
const existingIds :Set<string>= new Set(rawUpgrades.map(u=>u.id)); it("defines conditions with existing upgrades only", () => {
const missing:Set<string>=new Set(); const existingIds: Set<string> = new Set(rawUpgrades.map((u) => u.id));
Object.values(conditions).forEach(({required,forbidden})=>{ const missing: Set<string> = new Set();
[...required,...forbidden].forEach(id=> { Object.values(conditions).forEach(({ required, forbidden }) => {
if(!existingIds.has(id)) [...required, ...forbidden].forEach((id) => {
missing.add(id) if (!existingIds.has(id)) missing.add(id);
}) });
})
expect([...missing]).toEqual([]);
}); });
it("defines conditions for all levels", () => { expect([...missing]).toEqual([]);
const toAdd : Record<string,UnlockCondition>= {} });
levels.filter(l=>!l.name.startsWith('icon:')).forEach((l,li)=> {
if(l.name in conditions) return
toAdd[l.name]= getLevelUnlockCondition(li, l.name)
})
if(Object.keys(toAdd).length){
console.debug('Missing hardcoded conditons\n\n'+ JSON.stringify(toAdd).slice(1,-1)+'\n\n')
}
expect(Object.keys(toAdd)).toEqual([]);
});
}) it("defines conditions for all levels", () => {
const toAdd: Record<string, UnlockCondition> = {};
levels
.filter((l) => !l.name.startsWith("icon:"))
.forEach((l, li) => {
if (l.name in conditions) return;
toAdd[l.name] = getLevelUnlockCondition(li, l.name);
});
if (Object.keys(toAdd).length) {
console.debug(
"Missing hardcoded conditons\n\n" +
JSON.stringify(toAdd).slice(1, -1) +
"\n\n",
);
}
expect(Object.keys(toAdd)).toEqual([]);
});
});

View file

@ -1 +1 @@
"29092809" "29095000"

View file

@ -450,14 +450,12 @@ h2.histogram-title strong {
user-select: none; user-select: none;
opacity: 1; opacity: 1;
border: 1px solid white; border: 1px solid white;
&.desktop{ &.desktop {
max-width: 300px; max-width: 300px;
} }
&.mobile{ &.mobile {
width: 95vw; width: 95vw;
left:2.5vw; left: 2.5vw;
} }
} }

View file

@ -98,7 +98,7 @@ import { getNearestUnlockHTML, 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;

View file

@ -6,7 +6,6 @@ import {
currentLevelInfo, currentLevelInfo,
describeLevel, describeLevel,
pickedUpgradesHTMl, pickedUpgradesHTMl,
} from "./game_utils"; } from "./game_utils";
import { import {
askForPersistentStorage, askForPersistentStorage,
@ -18,7 +17,10 @@ import { stopRecording } from "./recording";
import { asyncAlert } from "./asyncAlert"; import { asyncAlert } from "./asyncAlert";
import { editRawLevelList } from "./levelEditor"; import { editRawLevelList } from "./levelEditor";
import { openCreativeModePerksPicker } from "./creative"; import { openCreativeModePerksPicker } from "./creative";
import {isLevelLocked, reasonLevelIsLocked} from "./get_level_unlock_condition"; import {
isLevelLocked,
reasonLevelIsLocked,
} from "./get_level_unlock_condition";
export function addToTotalPlayTime(ms: number) { export function addToTotalPlayTime(ms: number) {
setSettingValue( setSettingValue(
@ -140,7 +142,7 @@ export function getHistograms(gameState: GameState) {
.map((l, li) => ({ .map((l, li) => ({
li, li,
l, l,
r: reasonLevelIsLocked(li,l.name, runsHistory, false)?.text, r: reasonLevelIsLocked(li, l.name, runsHistory, false)?.text,
})) }))
.filter((l) => l.r); .filter((l) => l.r);
@ -160,7 +162,7 @@ export function getHistograms(gameState: GameState) {
}); });
const unlocked = locked.filter( const unlocked = locked.filter(
({ li ,l}) => !isLevelLocked(li, l.name, runsHistory), ({ li, l }) => !isLevelLocked(li, l.name, runsHistory),
); );
if (unlocked.length) { if (unlocked.length) {
unlockedLevels = ` unlockedLevels = `

View file

@ -1265,7 +1265,6 @@ export function gameStateTick(
// If you dont have buoy, we directly declare the coin "lost" to make it clear // If you dont have buoy, we directly declare the coin "lost" to make it clear
resetCombo(gameState, coin.x, coin.y); resetCombo(gameState, coin.x, coin.y);
} }
} }
if ( if (
@ -1659,7 +1658,7 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
speedLimitDampener += 3; speedLimitDampener += 3;
ball.vx += ball.vx +=
(gameState.puckPosition > ball.x ? 1 :-1) * (gameState.puckPosition > ball.x ? 1 : -1) *
frames * frames *
yoyoEffectRate(gameState, ball); yoyoEffectRate(gameState, ball);
} }

View file

@ -1,9 +1,9 @@
import {Ball, Coin, GameState, Level, PerkId, PerksMap,} from "./types"; import { Ball, Coin, GameState, Level, PerkId, PerksMap } from "./types";
import {icons, upgrades} from "./loadGameData"; import { icons, upgrades } from "./loadGameData";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {clamp} from "./pure_functions"; import { clamp } from "./pure_functions";
import {getSettingValue, getTotalScore} from "./settings"; import { getSettingValue, getTotalScore } from "./settings";
import {isOptionOn} from "./options"; import { isOptionOn } from "./options";
export function describeLevel(level: Level) { export function describeLevel(level: Level) {
let bricks = 0, let bricks = 0,
@ -192,10 +192,13 @@ export function telekinesisEffectRate(gameState: GameState, ball: Ball) {
} }
export function yoyoEffectRate(gameState: GameState, ball: Ball) { export function yoyoEffectRate(gameState: GameState, ball: Ball) {
if(ball.vy < 0) return 0 if (ball.vy < 0) return 0;
if(!gameState.perks.yoyo) return 0 if (!gameState.perks.yoyo) return 0;
return Math.abs(gameState.puckPosition - ball.x)/gameState.gameZoneWidth * gameState.perks.yoyo/2 return (
((Math.abs(gameState.puckPosition - ball.x) / gameState.gameZoneWidth) *
gameState.perks.yoyo) /
2
);
} }
export function findLast<T>( export function findLast<T>(

View file

@ -1,113 +1,123 @@
import {PerkId, RunHistoryItem, UnlockCondition} from "./types"; import { PerkId, RunHistoryItem, UnlockCondition } from "./types";
import {upgrades} from "./loadGameData"; import { upgrades } from "./loadGameData";
import {hashCode} from "./getLevelBackground"; import { hashCode } from "./getLevelBackground";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import _hardCodedCondition from './data/unlockConditions.json' import _hardCodedCondition from "./data/unlockConditions.json";
const hardCodedCondition = _hardCodedCondition as Record<string, UnlockCondition> const hardCodedCondition = _hardCodedCondition as Record<
string,
UnlockCondition
>;
let excluded: Set<PerkId>; let excluded: Set<PerkId>;
function isExcluded(id: PerkId) { function isExcluded(id: PerkId) {
if (!excluded) { if (!excluded) {
excluded = new Set([ excluded = new Set([
"extra_levels", "extra_levels",
"extra_life", "extra_life",
"one_more_choice", "one_more_choice",
"shunt", "shunt",
"slow_down", "slow_down",
]); ]);
// Avoid excluding a perk that's needed for the required one // Avoid excluding a perk that's needed for the required one
upgrades.forEach((u) => { upgrades.forEach((u) => {
if (u.requires) excluded.add(u.requires); if (u.requires) excluded.add(u.requires);
}); });
} }
return excluded.has(id); return excluded.has(id);
} }
export function getLevelUnlockCondition(levelIndex: number, levelName:string):UnlockCondition { export function getLevelUnlockCondition(
levelIndex: number,
levelName: string,
): UnlockCondition {
if (hardCodedCondition[levelName]) return hardCodedCondition[levelName];
const result: UnlockCondition = {
required: [],
forbidden: [],
minScore: Math.max(-1000 + 100 * levelIndex, 0),
};
if(hardCodedCondition[levelName]) return hardCodedCondition[levelName] if (levelIndex > 20) {
const result :UnlockCondition = { const possibletargets = [...upgrades]
required:[], .slice(0, Math.floor(levelIndex / 2))
forbidden:[], .filter((u) => !isExcluded(u.id))
minScore : Math.max(-1000 + 100 * levelIndex, 0) .sort((a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id))
} .map((u) => u.id);
if (levelIndex > 20) { const length = Math.min(3, Math.ceil(levelIndex / 30));
const possibletargets = [...upgrades] result.required = possibletargets.slice(0, length);
.slice(0, Math.floor(levelIndex / 2)) result.forbidden = possibletargets.slice(length, length + length);
.filter((u) => !isExcluded(u.id)) }
.sort( return result;
(a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id),
).map(u => u.id);
const length = Math.min(3, Math.ceil(levelIndex / 30));
result.required = possibletargets.slice(0, length);
result.forbidden = possibletargets.slice(length, length + length);
}
return result
} }
export function getBestScoreMatching( export function getBestScoreMatching(
history: RunHistoryItem[], history: RunHistoryItem[],
required: PerkId[] = [], required: PerkId[] = [],
forbidden: PerkId[] = [], forbidden: PerkId[] = [],
) { ) {
return Math.max( return Math.max(
0, 0,
...history ...history
.filter( .filter(
(r) => (r) =>
!required.find((id) => !r?.perks?.[id]) && !required.find((id) => !r?.perks?.[id]) &&
!forbidden.find((id) => r?.perks?.[id]), !forbidden.find((id) => r?.perks?.[id]),
) )
.map((r) => r.score), .map((r) => r.score),
); );
} }
export function isLevelLocked( export function isLevelLocked(
levelIndex: number, levelIndex: number,
levelName:string, levelName: string,
history: RunHistoryItem[]){ history: RunHistoryItem[],
const {required, forbidden, minScore} = getLevelUnlockCondition(levelIndex, levelName); ) {
return getBestScoreMatching(history, required, forbidden) < minScore const { required, forbidden, minScore } = getLevelUnlockCondition(
levelIndex,
levelName,
);
return getBestScoreMatching(history, required, forbidden) < minScore;
} }
export function reasonLevelIsLocked( export function reasonLevelIsLocked(
levelIndex: number,levelName:string, levelIndex: number,
history: RunHistoryItem[], levelName: string,
mentionBestScore: boolean, history: RunHistoryItem[],
mentionBestScore: boolean,
): null | { reached: number; minScore: number; text: string } { ): null | { reached: number; minScore: number; text: string } {
const { required, forbidden, minScore } = getLevelUnlockCondition(
const {required, forbidden, minScore} = getLevelUnlockCondition(levelIndex, levelName); levelIndex,
const reached = getBestScoreMatching(history, required, forbidden); levelName,
let reachedText = );
reached && mentionBestScore ? t("unlocks.reached", {reached}) : ""; const reached = getBestScoreMatching(history, required, forbidden);
if (reached >= minScore) { let reachedText =
return null; reached && mentionBestScore ? t("unlocks.reached", { reached }) : "";
} else if (!required.length && !forbidden.length) { if (reached >= minScore) {
return { return null;
reached, } else if (!required.length && !forbidden.length) {
minScore, return {
text: t("unlocks.minScore", {minScore}) + reachedText, reached,
}; minScore,
} else { text: t("unlocks.minScore", { minScore }) + reachedText,
return { };
reached, } else {
minScore, return {
text: reached,
t("unlocks.minScoreWithPerks", { minScore,
minScore, text:
required: required.map((u) => upgradeName(u)).join(", "), t("unlocks.minScoreWithPerks", {
forbidden: forbidden.map((u) => upgradeName(u)).join(", "), minScore,
}) + reachedText, required: required.map((u) => upgradeName(u)).join(", "),
}; forbidden: forbidden.map((u) => upgradeName(u)).join(", "),
} }) + reachedText,
};
}
} }
export function upgradeName(id:PerkId){ export function upgradeName(id: PerkId) {
return upgrades.find(u=>u.id==id)!.name return upgrades.find((u) => u.id == id)!.name;
} }

View file

@ -14,7 +14,7 @@ import {
MAX_LEVEL_SIZE, MAX_LEVEL_SIZE,
MIN_LEVEL_SIZE, MIN_LEVEL_SIZE,
} from "./pure_functions"; } from "./pure_functions";
import {toast} from "./toast"; import { toast } from "./toast";
const palette = _palette as Palette; const palette = _palette as Palette;
@ -234,7 +234,7 @@ export async function editRawLevelList(nth: number, color = "W") {
}); });
return; return;
} }
if (action === "copy" || action ==='show_code') { if (action === "copy" || action === "show_code") {
let text = let text =
"```\n[" + "```\n[" +
(level.name || "unnamed level")?.replace(/\[|\]/gi, " ") + (level.name || "unnamed level")?.replace(/\[|\]/gi, " ") +
@ -249,21 +249,23 @@ export async function editRawLevelList(nth: number, color = "W") {
"]\n```"; "]\n```";
if (action === "copy") { if (action === "copy") {
try{ try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
toast(t('editor.editing.copied')) toast(t("editor.editing.copied"));
}catch (e){ } catch (e) {
if('message' in e) { if ("message" in e) {
toast(e.message) toast(e.message);
} }
} }
}else{ } else {
await asyncAlert({ await asyncAlert({
title:t('editor.editing.show_code'), title: t("editor.editing.show_code"),
content:[` content: [
`
<pre>${text}</pre> <pre>${text}</pre>
`] `,
}) ],
});
} }
// return // return
} }

View file

@ -1,4 +1,4 @@
import { Palette, RawLevel } from "../types"; import { Palette, RawLevel } from "../types";
import _palette from "../data/palette.json"; import _palette from "../data/palette.json";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";

View file

@ -4,7 +4,10 @@ import _appVersion from "./data/version.json";
import { generateSaveFileContent } from "./generateSaveFileContent"; import { generateSaveFileContent } from "./generateSaveFileContent";
import { allLevels } from "./loadGameData"; import { allLevels } from "./loadGameData";
import { toast } from "./toast"; import { toast } from "./toast";
import {isLevelLocked, reasonLevelIsLocked} from "./get_level_unlock_condition"; import {
isLevelLocked,
reasonLevelIsLocked,
} from "./get_level_unlock_condition";
// The page will be reloaded if any migrations were run // The page will be reloaded if any migrations were run
let migrationsRun = 0; let migrationsRun = 0;
@ -128,7 +131,7 @@ migrate("set_breakout_71_unlocked_levels" + _appVersion, () => {
) as string[]; ) as string[];
allLevels allLevels
.filter((l, li) => !isLevelLocked(li,l.name, runsHistory)) .filter((l, li) => !isLevelLocked(li, l.name, runsHistory))
.forEach((l) => { .forEach((l) => {
if (!breakout_71_unlocked_levels.includes(l.name)) { if (!breakout_71_unlocked_levels.includes(l.name)) {
breakout_71_unlocked_levels.push(l.name); breakout_71_unlocked_levels.push(l.name);

View file

@ -1,22 +1,22 @@
import {GameState, PerkId} from "./types"; import { GameState, PerkId } from "./types";
import { getSettingValue, setSettingValue } from "./settings"; import { getSettingValue, setSettingValue } from "./settings";
import { allLevels, icons } from "./loadGameData"; import { allLevels, icons } from "./loadGameData";
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import { toast } from "./toast"; import { toast } from "./toast";
import { schedulGameSound } from "./gameStateMutators"; import { schedulGameSound } from "./gameStateMutators";
import {getLevelUnlockCondition} from "./get_level_unlock_condition"; import { getLevelUnlockCondition } from "./get_level_unlock_condition";
let list: { let list: {
minScore: number; minScore: number;
forbidden: PerkId[]; forbidden: PerkId[];
required: PerkId[]; required: PerkId[];
}[]; }[];
let unlocked : Set<string> |null = null let unlocked: Set<string> | null = null;
export function monitorLevelsUnlocks(gameState: GameState) { export function monitorLevelsUnlocks(gameState: GameState) {
if(!unlocked){ if (!unlocked) {
unlocked = new Set( unlocked = new Set(
getSettingValue("breakout_71_unlocked_levels", []) as string[], getSettingValue("breakout_71_unlocked_levels", []) as string[],
); );
} }

View file

@ -13,7 +13,10 @@ import { isOptionOn } from "./options";
import { getHistory } from "./gameOver"; import { getHistory } from "./gameOver";
import { getSettingValue, getTotalScore } from "./settings"; import { getSettingValue, getTotalScore } from "./settings";
import { isBlackListedForStart, isStartingPerk } from "./startingPerks"; import { isBlackListedForStart, isStartingPerk } from "./startingPerks";
import {isLevelLocked, reasonLevelIsLocked} from "./get_level_unlock_condition"; import {
isLevelLocked,
reasonLevelIsLocked,
} from "./get_level_unlock_condition";
export function getRunLevels( export function getRunLevels(
params: RunParams, params: RunParams,

View file

@ -1,17 +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 { import { levelsListHTMl, max_levels, pickedUpgradesHTMl } from "./game_utils";
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);
@ -41,10 +41,13 @@ export function getNearestUnlockHTML(gameState: GameState) {
const unlocked = new Set(getSettingValue("breakout_71_unlocked_levels", [])); const unlocked = new Set(getSettingValue("breakout_71_unlocked_levels", []));
const firstUnlockable = firstWhere(allLevels, (l, li) => { const firstUnlockable = firstWhere(allLevels, (l, li) => {
if (unlocked.has(l.name)) return; if (unlocked.has(l.name)) return;
const reason = reasonLevelIsLocked(li, l.name, getHistory(), false); const reason = reasonLevelIsLocked(li, l.name, getHistory(), false);
if (!reason) return; if (!reason) return;
const { minScore, forbidden, required } = getLevelUnlockCondition(li, l.name); const { minScore, forbidden, required } = getLevelUnlockCondition(
li,
l.name,
);
const missing = required.filter((id) => !gameState?.perks?.[id]); const missing = required.filter((id) => !gameState?.perks?.[id]);
// we can't have a forbidden perk // we can't have a forbidden perk
if (forbidden.find((id) => gameState?.perks?.[id])) { if (forbidden.find((id) => gameState?.perks?.[id])) {
@ -69,7 +72,9 @@ export function getNearestUnlockHTML(gameState: GameState) {
if (!firstUnlockable) return ""; if (!firstUnlockable) return "";
let missingPoints = Math.max(0, firstUnlockable.minScore - gameState.score); let missingPoints = Math.max(0, firstUnlockable.minScore - gameState.score);
let missingUpgrades = firstUnlockable.missing.map((id) => upgradeName(id)).join(", "); let missingUpgrades = firstUnlockable.missing
.map((id) => upgradeName(id))
.join(", ");
const title = const title =
(missingUpgrades && (missingUpgrades &&

View file

@ -12,8 +12,10 @@ export const options = {
help: t("settings.sounds_help"), help: t("settings.sounds_help"),
}, },
"mobile-mode": { "mobile-mode": {
default: window.innerHeight > window.innerWidth ||('ontouchstart' in window) || default:
(navigator.maxTouchPoints > 0) , window.innerHeight > window.innerWidth ||
"ontouchstart" in window ||
navigator.maxTouchPoints > 0,
name: t("settings.mobile"), name: t("settings.mobile"),
help: t("settings.mobile_help"), help: t("settings.mobile_help"),
}, },

View file

@ -19,7 +19,8 @@ import { isOptionOn } from "./options";
import { import {
ballTransparency, ballTransparency,
catchRateBest, catchRateBest,
catchRateGood, clamp, catchRateGood,
clamp,
coinsBoostedCombo, coinsBoostedCombo,
levelTimeBest, levelTimeBest,
levelTimeGood, levelTimeGood,
@ -401,11 +402,14 @@ export function render(gameState: GameState) {
) { ) {
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight); ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
ctx.globalAlpha =clamp( ctx.globalAlpha = clamp(
Math.max( Math.max(
telekinesisEffectRate(gameState, ball), telekinesisEffectRate(gameState, ball),
yoyoEffectRate(gameState, ball), yoyoEffectRate(gameState, ball),
) * ballAlpha,0,1); ) * ballAlpha,
0,
1,
);
ctx.strokeStyle = gameState.puckColor; ctx.strokeStyle = gameState.puckColor;
ctx.bezierCurveTo( ctx.bezierCurveTo(
gameState.puckPosition, gameState.puckPosition,

View file

@ -14,9 +14,9 @@ export function hideAnyTooltip() {
const tooltip = document.getElementById("tooltip") as HTMLDivElement; const tooltip = document.getElementById("tooltip") as HTMLDivElement;
function setupMobileTooltips(tooltip: HTMLDivElement) { function setupMobileTooltips(tooltip: HTMLDivElement) {
tooltip.className='mobile' tooltip.className = "mobile";
function openTooltip(e: Event) { function openTooltip(e: Event) {
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")) {
return; return;
@ -25,9 +25,8 @@ function setupMobileTooltips(tooltip: HTMLDivElement) {
e.preventDefault(); e.preventDefault();
tooltip.innerHTML = hovering.getAttribute("data-help-content") || ""; tooltip.innerHTML = hovering.getAttribute("data-help-content") || "";
tooltip.style.display = ""; tooltip.style.display = "";
const { top } = hovering.getBoundingClientRect(); const { top } = hovering.getBoundingClientRect();
tooltip.style.transform = `translate(0,${top}px) translate(0,-100%)`; tooltip.style.transform = `translate(0,${top}px) translate(0,-100%)`;
} }
document.body.addEventListener("touchstart", openTooltip, true); document.body.addEventListener("touchstart", openTooltip, true);
@ -62,7 +61,7 @@ function setupMobileTooltips(tooltip: HTMLDivElement) {
} }
function setupDesktopTooltips(tooltip: HTMLDivElement) { function setupDesktopTooltips(tooltip: HTMLDivElement) {
tooltip.className='desktop' tooltip.className = "desktop";
function updateTooltipPosition(e: { clientX: number; clientY: number }) { function updateTooltipPosition(e: { clientX: number; clientY: number }) {
tooltip.style.transform = `translate(${e.clientX}px,${e.clientY}px) translate(${e.clientX > window.innerWidth / 2 ? "-100%" : "0"},${e.clientY > (window.innerHeight * 2) / 3 ? "-100%" : "20px"})`; tooltip.style.transform = `translate(${e.clientX}px,${e.clientY}px) translate(${e.clientX > window.innerWidth / 2 ? "-100%" : "0"},${e.clientY > (window.innerHeight * 2) / 3 ? "-100%" : "20px"})`;
} }

19
src/types.d.ts vendored
View file

@ -1,5 +1,5 @@
import {rawUpgrades} from "./upgrades"; import { rawUpgrades } from "./upgrades";
import {options} from "./options"; import { options } from "./options";
export type colorString = string; export type colorString = string;
@ -301,14 +301,9 @@ export type OptionDef = {
help: string; help: string;
}; };
export type OptionId = keyof typeof options; export type OptionId = keyof typeof options;
export type UpgradeLike = {
id: PerkId;
name: string;
requires: string;
threshold: number;
};
export type UnlockCondition = { export type UnlockCondition = {
required: PerkId[]; required: PerkId[];
forbidden: PerkId[]; forbidden: PerkId[];
minScore: number; minScore: number;
} };

View file

@ -926,10 +926,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.10-0.05, lvl }), t("upgrades.passive_income.tooltip", { time: lvl * 0.1 - 0.05, lvl }),
fullHelp: (lvl: number) => fullHelp: (lvl: number) =>
t("upgrades.passive_income.verbose_description", { t("upgrades.passive_income.verbose_description", {
time: lvl * 0.10-0.05, time: lvl * 0.1 - 0.05,
lvl, lvl,
}), }),
}, },