mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-20 20:16:16 -04:00
Looping mode
This commit is contained in:
parent
3d5547e786
commit
5012076039
21 changed files with 2852 additions and 2696 deletions
46
Readme.md
46
Readme.md
|
@ -14,33 +14,39 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
|
|||
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
|
||||
|
||||
|
||||
# Todo
|
||||
|
||||
- people assume unbounded allows for wrap around
|
||||
- coin magnet and viscosity : only one level ~2.5
|
||||
- Boost Ascetism : give +2 or even +3 combo per brick destroyed
|
||||
- wind : move coins based on puck movement not position
|
||||
- show -N points in red when combo resets
|
||||
- reach : this is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
|
||||
- respawn: N% of bricks respawn after N seconds
|
||||
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
|
||||
- [jaceys] Move the restart button out of the menu, so that it is more easily accessible
|
||||
- [jaceys] A visual indication of whether a ball has hit a brick this serve
|
||||
- [obigre] Offer to level ups perks separately
|
||||
- bring back detailed help of perks as "intel"
|
||||
- https://weblate.org/fr/
|
||||
|
||||
|
||||
# Premium: allow looping
|
||||
|
||||
Allow players to loop the game :
|
||||
- [x] keep your score
|
||||
- [x] keep 1 perk
|
||||
- [x] add one hasard
|
||||
- [ ] add one HP to all bricks
|
||||
- [x] add one HP to all bricks - as a debuff
|
||||
- [ ] advertise looping in normal game over screen
|
||||
- [ ] save score at the end of first loop, in addition to the final one ?
|
||||
- [ ] check that stats like max level are correct
|
||||
- real time stats as the option says.
|
||||
- [x] Noise of coins against side is annoying.
|
||||
- Change look of loop, to avoid picking randomly at loop end.
|
||||
- make red coins scarier,
|
||||
- add blue coins that only freeze puck.
|
||||
- Make fullscreen an option and turn it back on when playing
|
||||
- +1 combo de base par rerolls
|
||||
- +1 combo de base par vie restantes (pas attrapable)
|
||||
|
||||
# Todo
|
||||
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
|
||||
|
||||
- people assume unbounded allows for wrap around
|
||||
- coin magnet and viscosity : only one level ~2.5
|
||||
- Boost Ascetism : give +2 or even +3 combo per brick destroyed
|
||||
- wind : move coins based on puck movement not position
|
||||
- show -N points in red when combo resets
|
||||
- reach is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
|
||||
- respawn: N% of bricks respawn after N seconds
|
||||
- [jaceys] Move the restart button out of the menu, so that it is more easily accessible
|
||||
- [jaceys] A visual indication of whether a ball has hit a brick this serve
|
||||
- [obigre] Offer to level ups perks separately
|
||||
- bring back detailed help of perks as "intel"
|
||||
- https://weblate.org/fr/
|
||||
|
||||
|
||||
|
||||
# System requirements
|
||||
|
|
|
@ -11,8 +11,8 @@ android {
|
|||
applicationId = "me.lecaro.breakout"
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 29050375
|
||||
versionName = "29050375"
|
||||
versionCode = 29053110
|
||||
versionName = "29053110"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
|
|
File diff suppressed because one or more lines are too long
102
dist/index.html
vendored
102
dist/index.html
vendored
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
|||
// The version of the cache.
|
||||
const VERSION = "29050375";
|
||||
const VERSION = "29053110";
|
||||
|
||||
// The name of the cache
|
||||
const CACHE_NAME = `breakout-71-${VERSION}`;
|
||||
|
|
|
@ -1 +1 @@
|
|||
"29050375"
|
||||
"29053110"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { t } from "./i18n/i18n";
|
||||
import {Debuff} from "./types";
|
||||
import { Debuff } from "./types";
|
||||
|
||||
export const debuffs = [
|
||||
{
|
||||
id: "negative_coins",
|
||||
max: 20,
|
||||
name: (lvl: number) => t("debuffs.negative_coins.help",{lvl}),
|
||||
name: (lvl: number) => t("debuffs.negative_coins.help", { lvl }),
|
||||
help: (lvl: number) => t("debuffs.negative_coins.help", { lvl }),
|
||||
},
|
||||
{
|
||||
|
@ -17,8 +17,10 @@ export const debuffs = [
|
|||
{
|
||||
id: "banned",
|
||||
max: 50,
|
||||
name: (lvl: number,banned:string) => t("debuffs.banned.description",{lvl,banned}),
|
||||
help: (lvl: number,perk:string) => t("debuffs.banned.help", { lvl,perk }),
|
||||
name: (lvl: number, banned: string) =>
|
||||
t("debuffs.banned.description", { lvl, banned }),
|
||||
help: (lvl: number, perk: string) =>
|
||||
t("debuffs.banned.help", { lvl, perk }),
|
||||
},
|
||||
{
|
||||
id: "interference",
|
||||
|
@ -30,8 +32,14 @@ export const debuffs = [
|
|||
{
|
||||
id: "fragility",
|
||||
max: 5,
|
||||
name: (lvl: number) => t("debuffs.fragility.help", { percent:lvl*20 }),
|
||||
help: (lvl: number) => t("debuffs.fragility.help", { percent:lvl*20 }),
|
||||
name: (lvl: number) => t("debuffs.fragility.help", { percent: lvl * 20 }),
|
||||
help: (lvl: number) => t("debuffs.fragility.help", { percent: lvl * 20 }),
|
||||
},
|
||||
{
|
||||
id: "sturdiness",
|
||||
max: 5,
|
||||
name: (lvl: number) => t("debuffs.sturdiness.help", { lvl }),
|
||||
help: (lvl: number) => t("debuffs.sturdiness.help", { lvl }),
|
||||
},
|
||||
] as const as Debuff[];
|
||||
|
||||
|
|
35
src/game.ts
35
src/game.ts
|
@ -14,7 +14,8 @@ import {
|
|||
import { getAudioContext, playPendingSounds } from "./sounds";
|
||||
import {
|
||||
bannedUpgradesHTMl,
|
||||
currentLevelInfo, debuffsHTMl,
|
||||
currentLevelInfo,
|
||||
debuffsHTMl,
|
||||
getRowColIndex,
|
||||
levelsListHTMl,
|
||||
max_levels,
|
||||
|
@ -447,15 +448,14 @@ document.addEventListener("visibilitychange", () => {
|
|||
async function openScorePanel() {
|
||||
pause(true);
|
||||
const cb = await asyncAlert({
|
||||
title:
|
||||
gameState.loop ?
|
||||
t("score_panel.title_looped", {
|
||||
loop:gameState.loop,
|
||||
title: gameState.loop
|
||||
? t("score_panel.title_looped", {
|
||||
loop: gameState.loop,
|
||||
score: gameState.score,
|
||||
level: gameState.currentLevel + 1,
|
||||
max: max_levels(gameState),
|
||||
}):
|
||||
t("score_panel.title", {
|
||||
})
|
||||
: t("score_panel.title", {
|
||||
score: gameState.score,
|
||||
level: gameState.currentLevel + 1,
|
||||
max: max_levels(gameState),
|
||||
|
@ -1013,22 +1013,23 @@ restart(
|
|||
// // unbounded: 1,
|
||||
// // pierce_color: 1,
|
||||
// pierce: 1,
|
||||
streak_shots:1,
|
||||
// streak_shots: 1,
|
||||
// multiball: 6,
|
||||
// base_combo: 7,
|
||||
// telekinesis: 2,
|
||||
// yoyo: 2,
|
||||
pierce:10,
|
||||
base_combo: 7,
|
||||
telekinesis: 2,
|
||||
yoyo: 2,
|
||||
pierce: 10,
|
||||
// metamorphosis: 1,
|
||||
// implosions: 1,
|
||||
// sturdy_bricks:5
|
||||
extra_life:3
|
||||
coin_magnet:2,
|
||||
extra_life: 3,
|
||||
},
|
||||
debuffs:{
|
||||
debuffs: {
|
||||
// fragility:3
|
||||
negative_coins:1
|
||||
// interference:20,
|
||||
}
|
||||
negative_coins: 100,
|
||||
// interference:20,
|
||||
},
|
||||
}) ||
|
||||
{},
|
||||
);
|
||||
|
|
|
@ -136,7 +136,7 @@ export function gameOver(title: string, intro: string) {
|
|||
],
|
||||
}).then(() =>
|
||||
restart({
|
||||
levelToAvoid: currentLevelInfo(gameState).name
|
||||
levelToAvoid: currentLevelInfo(gameState).name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -271,6 +271,7 @@ export function getHistograms() {
|
|||
(r) => r.max_combo,
|
||||
"",
|
||||
);
|
||||
runStats += makeHistogram(t("gameOver.stats.loops"), (r) => r.loops, "");
|
||||
|
||||
if (runStats) {
|
||||
runStats =
|
||||
|
|
|
@ -2,7 +2,10 @@ import {
|
|||
Ball,
|
||||
BallLike,
|
||||
Coin,
|
||||
colorString, Debuff, DebuffId, Debuffs,
|
||||
colorString,
|
||||
Debuff,
|
||||
DebuffId,
|
||||
Debuffs,
|
||||
GameState,
|
||||
LightFlash,
|
||||
ParticleFlash,
|
||||
|
@ -30,12 +33,16 @@ import {
|
|||
sample,
|
||||
shouldPierceByColor,
|
||||
} from "./game_utils";
|
||||
import {t} from "./i18n/i18n";
|
||||
import {icons, upgrades} from "./loadGameData";
|
||||
import { t } from "./i18n/i18n";
|
||||
import { icons, upgrades } from "./loadGameData";
|
||||
|
||||
import {addToTotalScore, getCurrentMaxCoins, getCurrentMaxParticles,} from "./settings";
|
||||
import {background} from "./render";
|
||||
import {gameOver} from "./gameOver";
|
||||
import {
|
||||
addToTotalScore,
|
||||
getCurrentMaxCoins,
|
||||
getCurrentMaxParticles,
|
||||
} from "./settings";
|
||||
import { background } from "./render";
|
||||
import { gameOver } from "./gameOver";
|
||||
import {
|
||||
brickIndex,
|
||||
fitSize,
|
||||
|
@ -46,12 +53,12 @@ import {
|
|||
pause,
|
||||
play,
|
||||
} from "./game";
|
||||
import {stopRecording} from "./recording";
|
||||
import {isOptionOn} from "./options";
|
||||
import {isPremium} from "./premium";
|
||||
import {getRunLevels} from "./newGameState";
|
||||
import {debuffs} from "./debuffs";
|
||||
import {requiredAsyncAlert} from "./asyncAlert";
|
||||
import { stopRecording } from "./recording";
|
||||
import { isOptionOn } from "./options";
|
||||
import { isPremium } from "./premium";
|
||||
import { getRunLevels } from "./newGameState";
|
||||
import { debuffs } from "./debuffs";
|
||||
import { requiredAsyncAlert } from "./asyncAlert";
|
||||
|
||||
export function setMousePos(gameState: GameState, x: number) {
|
||||
// Sets the puck position, and updates the ball position if they are supposed to follow it
|
||||
|
@ -175,7 +182,7 @@ export function normalizeGameState(gameState: GameState) {
|
|||
}
|
||||
|
||||
export function baseCombo(gameState: GameState) {
|
||||
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
|
||||
return gameState.baseCombo + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
|
||||
}
|
||||
|
||||
export function resetCombo(
|
||||
|
@ -333,15 +340,14 @@ export function explosionAt(
|
|||
if (gameState.perks.zen) {
|
||||
resetCombo(gameState, x, y);
|
||||
}
|
||||
if(gameState.debuffs.fragility){
|
||||
if (gameState.debuffs.fragility) {
|
||||
resetCombo(gameState, x, y);
|
||||
forEachLiveOne(gameState.coins, (coin, index)=>{
|
||||
forEachLiveOne(gameState.coins, (coin, index) => {
|
||||
// Also destroys cursed coins
|
||||
if(Math.random()<gameState.debuffs.fragility/5){
|
||||
destroy(gameState.coins, index)
|
||||
if (Math.random() < gameState.debuffs.fragility / 5) {
|
||||
destroy(gameState.coins, index);
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -395,7 +401,7 @@ export function explodeBrick(
|
|||
while (coinsToSpawn > 0) {
|
||||
const points = Math.min(pointsPerCoin, coinsToSpawn);
|
||||
if (points < 0 || isNaN(points)) {
|
||||
console.error({points});
|
||||
console.error({ points });
|
||||
debugger;
|
||||
}
|
||||
|
||||
|
@ -413,7 +419,9 @@ export function explodeBrick(
|
|||
cy,
|
||||
ball.previousVX * (0.5 + Math.random()),
|
||||
ball.previousVY * (0.5 + Math.random()),
|
||||
gameState.perks.metamorphosis || isOptionOn('colorful_coins') ? color : "gold",
|
||||
gameState.perks.metamorphosis || isOptionOn("colorful_coins")
|
||||
? color
|
||||
: "gold",
|
||||
points,
|
||||
);
|
||||
}
|
||||
|
@ -554,14 +562,11 @@ export function addToScore(gameState: GameState, coin: Coin) {
|
|||
gameState.score += coin.points;
|
||||
gameState.lastScoreIncrease = gameState.levelTime;
|
||||
addToTotalScore(gameState, coin.points);
|
||||
if (
|
||||
gameState.score > gameState.highScore &&
|
||||
!gameState.isCreativeModeRun
|
||||
) {
|
||||
if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) {
|
||||
gameState.highScore = gameState.score;
|
||||
localStorage.setItem("breakout-3-hs", gameState.score.toString());
|
||||
}
|
||||
if (!isOptionOn("basic") ) {
|
||||
if (!isOptionOn("basic")) {
|
||||
makeParticle(
|
||||
gameState,
|
||||
coin.previousX,
|
||||
|
@ -583,46 +588,67 @@ export function addToScore(gameState: GameState, coin: Coin) {
|
|||
}
|
||||
|
||||
export async function gotoNextLoop(gameState: GameState) {
|
||||
pause(false)
|
||||
gameState.loop++
|
||||
gameState.runStatistics.loops++
|
||||
gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {})
|
||||
gameState.upgradesOfferedFor = -1
|
||||
// Add random debuf
|
||||
pause(false);
|
||||
gameState.loop++;
|
||||
gameState.runStatistics.loops++;
|
||||
gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {});
|
||||
gameState.upgradesOfferedFor = -1;
|
||||
|
||||
// gameState.debuffs[randomDebuff]++
|
||||
const userPerks=upgrades.filter(u => gameState.perks[u.id])
|
||||
const {keep,
|
||||
debuff,
|
||||
targetPerk} = await requiredAsyncAlert<{ keep:PerkId, debuff:DebuffId, targetPerk:PerkId }>({
|
||||
title: t('loop.title', {loop: gameState.loop}),
|
||||
let comboText=''
|
||||
if(gameState.rerolls) {
|
||||
comboText=t('loop.converted_rerolls',{n:gameState.rerolls})
|
||||
gameState.baseCombo += gameState.rerolls
|
||||
gameState.rerolls=0
|
||||
}else{
|
||||
comboText=t('loop.no_rerolls')
|
||||
}
|
||||
|
||||
const userPerks = upgrades.filter((u) => gameState.perks[u.id]);
|
||||
const { keep, debuff, targetPerk } = await requiredAsyncAlert<{
|
||||
keep: PerkId;
|
||||
debuff: DebuffId;
|
||||
targetPerk: PerkId;
|
||||
}>({
|
||||
title: t("loop.title", { loop: gameState.loop }),
|
||||
content: [
|
||||
t('loop.instructions'),
|
||||
...userPerks
|
||||
.map(u => {
|
||||
const randomDebuff = sample(debuffs.filter(d => gameState.debuffs[d.id] < d.max)) || sample(debuffs);
|
||||
const targetPerk = sample(userPerks.filter(tp=>tp.id!==u.id))
|
||||
return ({
|
||||
text: u.name + t('level_up.upgrade_perk_to_level', {level: gameState.perks[u.id]}),
|
||||
help: randomDebuff.help(gameState.debuffs[randomDebuff.id]+1, targetPerk.name),
|
||||
t("loop.instructions"),
|
||||
comboText,
|
||||
|
||||
...userPerks.map((u) => {
|
||||
const randomDebuff =
|
||||
sample(debuffs.filter((d) => gameState.debuffs[d.id] < d.max)) ||
|
||||
sample(debuffs);
|
||||
const targetPerk = sample(userPerks.filter((tp) => tp.id !== u.id));
|
||||
return {
|
||||
text:
|
||||
u.name +
|
||||
t("level_up.upgrade_perk_to_level", {
|
||||
level: gameState.perks[u.id],
|
||||
}),
|
||||
help: randomDebuff.help(
|
||||
gameState.debuffs[randomDebuff.id] + 1,
|
||||
targetPerk.name,
|
||||
),
|
||||
icon: u.icon,
|
||||
value: {
|
||||
keep: u.id,
|
||||
debuff: randomDebuff.id,
|
||||
targetPerk:targetPerk.id
|
||||
}
|
||||
})
|
||||
})
|
||||
]
|
||||
})
|
||||
targetPerk: targetPerk.id,
|
||||
},
|
||||
};
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), {[keep]: gameState.perks[keep]})
|
||||
gameState.debuffs[debuff]++
|
||||
if(debuff=='banned'){
|
||||
gameState.bannedPerks[targetPerk]++
|
||||
Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), {
|
||||
[keep]: gameState.perks[keep],
|
||||
});
|
||||
gameState.debuffs[debuff]++;
|
||||
if (debuff == "banned") {
|
||||
gameState.bannedPerks[targetPerk]++;
|
||||
}
|
||||
|
||||
await setLevel(gameState, 0)
|
||||
await setLevel(gameState, 0);
|
||||
}
|
||||
|
||||
export async function setLevel(gameState: GameState, l: number) {
|
||||
|
@ -635,7 +661,6 @@ export async function setLevel(gameState: GameState, l: number) {
|
|||
pause(false);
|
||||
stopRecording();
|
||||
if (l > 0) {
|
||||
|
||||
await openShortRunUpgradesPicker(gameState);
|
||||
}
|
||||
gameState.currentLevel = l;
|
||||
|
@ -685,10 +710,7 @@ export async function setLevel(gameState: GameState, l: number) {
|
|||
while (attemps < 100 && changed < gameState.debuffs.more_bombs) {
|
||||
attemps++;
|
||||
const index = Math.floor(Math.random() * gameState.bricks.length);
|
||||
if (
|
||||
gameState.bricks[index] &&
|
||||
gameState.bricks[index] !== "black"
|
||||
) {
|
||||
if (gameState.bricks[index] && gameState.bricks[index] !== "black") {
|
||||
gameState.bricks[index] = "black";
|
||||
gameState.brickHP[index] = 1;
|
||||
changed++;
|
||||
|
@ -707,7 +729,8 @@ function setBrick(gameState: GameState, index: number, color: string) {
|
|||
gameState.bricks[index] = color || "";
|
||||
gameState.brickHP[index] =
|
||||
(color === "black" && 1) ||
|
||||
(color && 1 + gameState.perks.sturdy_bricks+gameState.loop) ||
|
||||
(color &&
|
||||
1 + gameState.perks.sturdy_bricks + gameState.debuffs.sturdiness) ||
|
||||
0;
|
||||
}
|
||||
|
||||
|
@ -823,7 +846,7 @@ export function attract(gameState: GameState, a: Ball, b: Ball, power: number) {
|
|||
export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
|
||||
// Make ball/coin bonce, and return bricks that were hit
|
||||
const radius = coin.size / 2;
|
||||
const {x, y, previousX, previousY} = coin;
|
||||
const { x, y, previousX, previousY } = coin;
|
||||
|
||||
const vhit = hitsSomething(previousX, y, radius);
|
||||
const hhit = hitsSomething(x, previousY, radius);
|
||||
|
@ -882,7 +905,7 @@ export function bordersHitCheck(
|
|||
(gameState.offsetX + gameState.gameZoneWidth / 2)) /
|
||||
gameState.gameZoneWidth) *
|
||||
gameState.perks.wind *
|
||||
0.5 ;
|
||||
0.5;
|
||||
}
|
||||
|
||||
let vhit = 0,
|
||||
|
@ -974,28 +997,22 @@ export function gameStateTick(
|
|||
}
|
||||
|
||||
if (
|
||||
gameState.running &&
|
||||
(gameState.running &&
|
||||
// Delayed win when coins are still flying
|
||||
(gameState.winAt && gameState.levelTime > gameState.winAt) ||
|
||||
gameState.winAt &&
|
||||
gameState.levelTime > gameState.winAt) ||
|
||||
// instant win condition
|
||||
(
|
||||
gameState.levelTime &&
|
||||
!remainingBricks &&
|
||||
!liveCount(gameState.coins))
|
||||
) {
|
||||
|
||||
if (
|
||||
gameState.currentLevel + 1 < max_levels(gameState)
|
||||
(gameState.levelTime && !remainingBricks && !liveCount(gameState.coins))
|
||||
) {
|
||||
if (gameState.currentLevel + 1 < max_levels(gameState)) {
|
||||
setLevel(gameState, gameState.currentLevel + 1);
|
||||
} else {
|
||||
if (isPremium()) {
|
||||
gotoNextLoop(gameState)
|
||||
gotoNextLoop(gameState);
|
||||
} else {
|
||||
|
||||
gameOver(
|
||||
t("gameOver.win.title"),
|
||||
t("gameOver.win.summary", {score: gameState.score}),
|
||||
t("gameOver.win.summary", { score: gameState.score }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1030,7 +1047,11 @@ export function gameStateTick(
|
|||
});
|
||||
}
|
||||
|
||||
const ratio = 1 - ((coin.color==='crimson' ? 2:gameState.perks.viscosity)* 0.03 + 0.005) * frames;
|
||||
const ratio =
|
||||
1 -
|
||||
((coin.color === "crimson" ? 3 : gameState.perks.viscosity) * 0.03 +
|
||||
0.005) *
|
||||
frames;
|
||||
|
||||
coin.vy *= ratio;
|
||||
coin.vx *= ratio;
|
||||
|
@ -1064,6 +1085,21 @@ export function gameStateTick(
|
|||
}
|
||||
}
|
||||
|
||||
if(coin.color === "crimson" && !isOptionOn('basic')){
|
||||
const angle=Math.random()*Math.PI*2
|
||||
makeParticle(
|
||||
gameState,
|
||||
coin.x,
|
||||
coin.y,
|
||||
Math.cos(angle)*gameState.baseSpeed*2,
|
||||
Math.sin(angle)*gameState.baseSpeed*2,
|
||||
'red',
|
||||
true,
|
||||
5,
|
||||
250,
|
||||
);
|
||||
}
|
||||
|
||||
const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10;
|
||||
const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
|
||||
|
||||
|
@ -1076,15 +1112,15 @@ export function gameStateTick(
|
|||
// a bit of margin to be nice , negative in case it's a negative coin
|
||||
gameState.puckHeight * (coin.points ? 1 : -1)
|
||||
) {
|
||||
if(coin.points) {
|
||||
if (coin.points) {
|
||||
addToScore(gameState, coin);
|
||||
}else if(gameState.perks.extra_life && gameState.balls.length){
|
||||
justLostALife(gameState, gameState.balls[0], coin.x,coin.y)
|
||||
}else{
|
||||
} else if (gameState.perks.extra_life && gameState.balls.length) {
|
||||
justLostALife(gameState, gameState.balls[0], coin.x, coin.y);
|
||||
} else {
|
||||
gameOver(
|
||||
t('gameOver.because_cursed_coin'),
|
||||
t('gameOver.because_cursed_coin_intro')
|
||||
)
|
||||
t("gameOver.because_cursed_coin"),
|
||||
t("gameOver.because_cursed_coin_intro"),
|
||||
);
|
||||
}
|
||||
destroy(gameState.coins, coinIndex);
|
||||
} else if (coin.y > gameState.canvasHeight + coinRadius) {
|
||||
|
@ -1102,10 +1138,7 @@ export function gameStateTick(
|
|||
}
|
||||
|
||||
const hitBrick = coinBrickHitCheck(gameState, coin);
|
||||
if (
|
||||
gameState.perks.metamorphosis &&
|
||||
typeof hitBrick !== "undefined"
|
||||
) {
|
||||
if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
|
||||
if (
|
||||
gameState.bricks[hitBrick] &&
|
||||
coin.color !== gameState.bricks[hitBrick] &&
|
||||
|
@ -1127,20 +1160,21 @@ export function gameStateTick(
|
|||
coin.vx *= 0.8;
|
||||
coin.vy *= 0.8;
|
||||
coin.sa *= 0.9;
|
||||
if (speed > 20) {
|
||||
if (speed > 20 && !coin.collidedLastFrame) {
|
||||
schedulGameSound(gameState, "coinBounce", coin.x, 0.2);
|
||||
}
|
||||
coin.collidedLastFrame = true;
|
||||
|
||||
if (Math.abs(coin.vy) < 3) {
|
||||
coin.vy = 0;
|
||||
}
|
||||
} else {
|
||||
coin.collidedLastFrame = false;
|
||||
}
|
||||
});
|
||||
|
||||
gameState.balls.forEach((ball) => ballTick(gameState, ball, frames));
|
||||
|
||||
|
||||
|
||||
if (gameState.perks.shocks) {
|
||||
gameState.balls.forEach((a, ai) =>
|
||||
gameState.balls.forEach((b, bi) => {
|
||||
|
@ -1307,7 +1341,6 @@ export function gameStateTick(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
forEachLiveOne(gameState.particles, (p, pi) => {
|
||||
if (gameState.levelTime > p.time + p.duration) {
|
||||
destroy(gameState.particles, pi);
|
||||
|
@ -1341,15 +1374,16 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
|||
ball.vx +=
|
||||
((gameState.puckPosition - ball.x) / 1000) *
|
||||
delta *
|
||||
gameState.perks.telekinesis
|
||||
* interferenceFactor(gameState)
|
||||
;
|
||||
gameState.perks.telekinesis *
|
||||
interferenceFactor(gameState);
|
||||
}
|
||||
if (isYoyoActive(gameState, ball)) {
|
||||
speedLimitDampener += 3;
|
||||
ball.vx +=
|
||||
((gameState.puckPosition - ball.x) / 1000) * delta * gameState.perks.yoyo
|
||||
* interferenceFactor(gameState);
|
||||
((gameState.puckPosition - ball.x) / 1000) *
|
||||
delta *
|
||||
gameState.perks.yoyo *
|
||||
interferenceFactor(gameState);
|
||||
}
|
||||
if (
|
||||
ball.vx * ball.vx + ball.vy * ball.vy <
|
||||
|
@ -1408,7 +1442,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
|||
i < ball.hitItem?.length - 1 && i < gameState.perks.respawn;
|
||||
i++
|
||||
) {
|
||||
const {index, color} = ball.hitItem[i];
|
||||
const { index, color } = ball.hitItem[i];
|
||||
if (gameState.bricks[index] || color === "black") continue;
|
||||
const vertical = Math.random() > 0.5;
|
||||
const dx = Math.random() > 0.5 ? 1 : -1;
|
||||
|
@ -1493,8 +1527,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
|||
schedulGameSound(gameState, "wallBeep", ball.x, 1);
|
||||
} else {
|
||||
ball.vy *= -1;
|
||||
justLostALife(gameState, ball, ball.x,ball.y)
|
||||
|
||||
justLostALife(gameState, ball, ball.x, ball.y);
|
||||
}
|
||||
if (gameState.perks.streak_shots) {
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
|
@ -1513,7 +1546,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
|||
ball.hitItem
|
||||
.slice(0, -1)
|
||||
.slice(0, gameState.perks.respawn)
|
||||
.forEach(({index, color}) => {
|
||||
.forEach(({ index, color }) => {
|
||||
if (!gameState.bricks[index] && color !== "black") {
|
||||
// respawns with full hp
|
||||
setBrick(gameState, index, color);
|
||||
|
@ -1563,13 +1596,13 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
|||
if (!gameState.balls.find((b) => !b.destroyed)) {
|
||||
gameOver(
|
||||
t("gameOver.lost.title"),
|
||||
t("gameOver.lost.summary", {score: gameState.score}),
|
||||
t("gameOver.lost.summary", { score: gameState.score }),
|
||||
);
|
||||
}
|
||||
}
|
||||
const radius = gameState.ballSize / 2;
|
||||
// Make ball/coin bonce, and return bricks that were hit
|
||||
const {x, y, previousX, previousY} = ball;
|
||||
const { x, y, previousX, previousY } = ball;
|
||||
|
||||
const vhit = hitsSomething(previousX, y, radius);
|
||||
const hhit = hitsSomething(x, previousY, radius);
|
||||
|
@ -1629,11 +1662,16 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
|||
setBrick(gameState, hitBrick, "black");
|
||||
ball.sapperUses++;
|
||||
}
|
||||
}else{
|
||||
schedulGameSound(gameState, 'wallBeep',x,1)
|
||||
makeLight(gameState, brickCenterX(gameState, hitBrick),
|
||||
brickCenterY(gameState,hitBrick), "white", gameState.brickWidth+2 ,
|
||||
50*gameState.brickHP[hitBrick]);
|
||||
} else {
|
||||
schedulGameSound(gameState, "wallBeep", x, 1);
|
||||
makeLight(
|
||||
gameState,
|
||||
brickCenterX(gameState, hitBrick),
|
||||
brickCenterY(gameState, hitBrick),
|
||||
"white",
|
||||
gameState.brickWidth + 2,
|
||||
50 * gameState.brickHP[hitBrick],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1671,7 +1709,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
|||
}
|
||||
}
|
||||
|
||||
function justLostALife(gameState:GameState, ball:Ball, x:number,y:number){
|
||||
function justLostALife(gameState: GameState, ball: Ball, x: number, y: number) {
|
||||
gameState.perks.extra_life -= 1;
|
||||
if (gameState.perks.extra_life < 0) {
|
||||
gameState.perks.extra_life = 0;
|
||||
|
@ -1697,7 +1735,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
|||
150,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeCoin(
|
||||
gameState: GameState,
|
||||
|
@ -1708,13 +1746,17 @@ function makeCoin(
|
|||
color = "gold",
|
||||
points = 1,
|
||||
) {
|
||||
if (gameState.debuffs.negative_coins *points> Math.random() * 10000) {
|
||||
if (y<gameState.gameZoneWidth*2/3 &&
|
||||
gameState.debuffs.negative_coins * points > Math.random() * 10000) {
|
||||
points = 0;
|
||||
color = "crimson";
|
||||
vx=0
|
||||
vy=0
|
||||
}
|
||||
append(gameState.coins, (p: Partial<Coin>) => {
|
||||
p.x = x;
|
||||
p.y = y;
|
||||
p.collidedLastFrame = true;
|
||||
p.size = gameState.coinSize;
|
||||
p.previousX = x;
|
||||
p.previousY = y;
|
||||
|
@ -1730,11 +1772,11 @@ function makeCoin(
|
|||
});
|
||||
}
|
||||
|
||||
export function interferenceFactor(gameState:GameState){
|
||||
if(!gameState.debuffs.interference) return 1
|
||||
const cycleLength = (7+gameState.debuffs.interference)*1000
|
||||
const position = gameState.levelTime % cycleLength
|
||||
return position>7000 ? -1 :1
|
||||
export function interferenceFactor(gameState: GameState) {
|
||||
if (!gameState.debuffs.interference) return 1;
|
||||
const cycleLength = (7 + gameState.debuffs.interference) * 1000;
|
||||
const position = gameState.levelTime % cycleLength;
|
||||
return position > 7000 ? -1 : 1;
|
||||
}
|
||||
|
||||
function makeParticle(
|
||||
|
@ -1815,7 +1857,7 @@ export function append<T>(
|
|||
makeItem(where.list[where.indexMin]);
|
||||
where.indexMin++;
|
||||
} else {
|
||||
const p = {destroyed: false};
|
||||
const p = { destroyed: false };
|
||||
makeItem(p);
|
||||
where.list.push(p);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Ball, GameState, PerkId, PerksMap } from "./types";
|
||||
import { icons, upgrades } from "./loadGameData";
|
||||
import { t } from "./i18n/i18n";
|
||||
import {debuffs} from "./debuffs";
|
||||
import { debuffs } from "./debuffs";
|
||||
|
||||
export function getMajorityValue(arr: string[]): string {
|
||||
const count: { [k: string]: number } = {};
|
||||
|
@ -55,7 +55,6 @@ export function getPossibleUpgrades(gameState: GameState) {
|
|||
}
|
||||
|
||||
export function max_levels(gameState: GameState) {
|
||||
|
||||
return 7 + gameState.perks.extra_levels;
|
||||
}
|
||||
|
||||
|
@ -70,10 +69,15 @@ export function pickedUpgradesHTMl(gameState: GameState) {
|
|||
return ` <p>${t("score_panel.upgrades_picked")}</p> <p>${list}</p>`;
|
||||
}
|
||||
|
||||
|
||||
export function debuffsHTMl(gameState: GameState):string {
|
||||
const banned = upgrades.filter(u=>gameState.bannedPerks[u.id]).map(u=>u.name).join(', ')
|
||||
let list = debuffs.filter(d=>gameState.debuffs[d.id]).map(d=>d.name(gameState.debuffs[d.id], banned)).join(' ');
|
||||
export function debuffsHTMl(gameState: GameState): string {
|
||||
const banned = upgrades
|
||||
.filter((u) => gameState.bannedPerks[u.id])
|
||||
.map((u) => u.name)
|
||||
.join(", ");
|
||||
let list = debuffs
|
||||
.filter((d) => gameState.debuffs[d.id])
|
||||
.map((d) => d.name(gameState.debuffs[d.id], banned))
|
||||
.join(" ");
|
||||
|
||||
if (!list) return "";
|
||||
return `<p>${t("score_panel.bebuffs_list")} ${list}</p>`;
|
||||
|
|
|
@ -202,6 +202,26 @@
|
|||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
<name>sturdiness</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>help</name>
|
||||
<description/>
|
||||
<comment/>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-FR</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
|
@ -470,6 +490,21 @@
|
|||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>loops</name>
|
||||
<description/>
|
||||
<comment/>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-FR</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>total_score</name>
|
||||
<description/>
|
||||
|
@ -787,6 +822,21 @@
|
|||
<folder_node>
|
||||
<name>loop</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>converted_rerolls</name>
|
||||
<description/>
|
||||
<comment/>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-FR</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>instructions</name>
|
||||
<description/>
|
||||
|
@ -802,6 +852,21 @@
|
|||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>no_rerolls</name>
|
||||
<description/>
|
||||
<comment/>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-FR</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>title</name>
|
||||
<description/>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"debuffs.interference.help": "Telekinesis and yo-yo glitch for {{lvl}}s every 7s.",
|
||||
"debuffs.more_bombs.help": "{{lvl}} bricks replaced by bombs.",
|
||||
"debuffs.negative_coins.help": "{{lvl}}/10000 of coins spawn cursed, blinking red. Game over if you catch them.",
|
||||
"debuffs.sturdiness.help": "All bricks have +{{lvl}} HP",
|
||||
"gameOver.because_cursed_coin": "Game over",
|
||||
"gameOver.because_cursed_coin_intro": "You cough a cursed coin (bright red coins) and didn't have a extra life to spare. ",
|
||||
"gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
|
||||
|
@ -26,6 +27,7 @@
|
|||
"gameOver.stats.hit_rate": "Hit rate",
|
||||
"gameOver.stats.intro": "Find below your run statistics compared to your {{count}} best runs.",
|
||||
"gameOver.stats.level_reached": "Level reached",
|
||||
"gameOver.stats.loops": "Loops",
|
||||
"gameOver.stats.total_score": "Total score",
|
||||
"gameOver.stats.upgrades_applied": "Upgrades applied",
|
||||
"gameOver.test_run": "This test run and its score are not being recorded",
|
||||
|
@ -46,7 +48,9 @@
|
|||
"level_up.unlocked_level": " (Level)",
|
||||
"level_up.unlocked_perk": " (Perk)",
|
||||
"level_up.upgrade_perk_to_level": " lvl {{level}}",
|
||||
"loop.converted_rerolls": "Your {{n}} leftover re-rolls where converted to +{{n}} base combo.",
|
||||
"loop.instructions": "All your perks will be erased except one that you can pick below. Each option comes with an additional hazard will appear on all levels going forward. ",
|
||||
"loop.no_rerolls": "You didn't have any leftover re-rolls, so your base combo stayed the same. ",
|
||||
"loop.title": "Starting loop {{loop}}",
|
||||
"main_menu.basic": "Basic graphics",
|
||||
"main_menu.basic_help": "Better performance.",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"debuffs.interference.help": "Télékinésie et problème de yo-yo pendant {{lvl}}s toutes les 7 s.",
|
||||
"debuffs.more_bombs.help": "{{lvl}} briques remplacées par des bombes.",
|
||||
"debuffs.negative_coins.help": "{{lvl}}/10000 pièces apparaissent maudites et clignotent en rouge. La partie est terminée si vous les attrapez.",
|
||||
"debuffs.sturdiness.help": "Toutes les briques résistent à +{{lvl}} chocs",
|
||||
"gameOver.because_cursed_coin": "Jeu terminé",
|
||||
"gameOver.because_cursed_coin_intro": "Vous avez craché une pièce maudite (pièces rouge vif) et vous n'aviez pas de vie supplémentaire à revendre.",
|
||||
"gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
|
||||
|
@ -26,6 +27,7 @@
|
|||
"gameOver.stats.hit_rate": "Précision",
|
||||
"gameOver.stats.intro": "Vous trouverez ci-dessous les statistiques de cette partie comparées à vos {{count}} meilleures parties.",
|
||||
"gameOver.stats.level_reached": "Niveau atteint",
|
||||
"gameOver.stats.loops": "Boucles",
|
||||
"gameOver.stats.total_score": "Score total",
|
||||
"gameOver.stats.upgrades_applied": "Mises à jour appliquées",
|
||||
"gameOver.test_run": "Cette partie de test et son score ne sont pas enregistrés.",
|
||||
|
@ -46,7 +48,9 @@
|
|||
"level_up.unlocked_level": " (Niveau)",
|
||||
"level_up.unlocked_perk": " (Amélioration)",
|
||||
"level_up.upgrade_perk_to_level": " niveau {{level}}",
|
||||
"loop.converted_rerolls": "",
|
||||
"loop.instructions": "Tous vos avantages seront supprimés, sauf un que vous pouvez choisir ci-dessous. Chaque option comporte un danger supplémentaire qui apparaîtra à tous les niveaux.",
|
||||
"loop.no_rerolls": "",
|
||||
"loop.title": "Boucle de départ {{loop}}",
|
||||
"main_menu.basic": "Graphismes simplifiés",
|
||||
"main_menu.basic_help": "Meilleures performances.",
|
||||
|
|
|
@ -45,5 +45,5 @@ export const allLevels = rawLevelsList
|
|||
|
||||
export const upgrades = rawUpgrades.map((u) => ({
|
||||
...u,
|
||||
icon: icons["icon:" + u.id]
|
||||
icon: icons["icon:" + u.id],
|
||||
})) as Upgrade[];
|
||||
|
|
|
@ -11,10 +11,9 @@ import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
|
|||
import { isOptionOn } from "./options";
|
||||
import { debuffs } from "./debuffs";
|
||||
|
||||
|
||||
export function getRunLevels(totalScoreAtRunStart:number, params: RunParams){
|
||||
const firstLevel =
|
||||
params?.level ? allLevels.filter((l) => l.name === params?.level)
|
||||
export function getRunLevels(totalScoreAtRunStart: number, params: RunParams) {
|
||||
const firstLevel = params?.level
|
||||
? allLevels.filter((l) => l.name === params?.level)
|
||||
: [];
|
||||
|
||||
const restInRandomOrder = allLevels
|
||||
|
@ -23,7 +22,7 @@ export function getRunLevels(totalScoreAtRunStart:number, params: RunParams){
|
|||
.filter((l) => l.name !== params?.levelToAvoid)
|
||||
.sort(() => Math.random() - 0.5);
|
||||
|
||||
return firstLevel.concat(
|
||||
return firstLevel.concat(
|
||||
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
|
||||
);
|
||||
}
|
||||
|
@ -31,7 +30,7 @@ return firstLevel.concat(
|
|||
export function newGameState(params: RunParams): GameState {
|
||||
const totalScoreAtRunStart = getTotalScore();
|
||||
|
||||
const runLevels =getRunLevels(totalScoreAtRunStart, params)
|
||||
const runLevels = getRunLevels(totalScoreAtRunStart, params);
|
||||
|
||||
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
|
||||
|
||||
|
@ -41,7 +40,7 @@ export function newGameState(params: RunParams): GameState {
|
|||
currentLevel: 0,
|
||||
upgradesOfferedFor: -1,
|
||||
perks,
|
||||
bannedPerks:makeEmptyPerksMap(upgrades),
|
||||
bannedPerks: makeEmptyPerksMap(upgrades),
|
||||
debuffs: { ...emptyDebuffsMap(), ...(params?.debuffs || {}) },
|
||||
puckWidth: 200,
|
||||
baseSpeed: 12,
|
||||
|
@ -110,7 +109,8 @@ export function newGameState(params: RunParams): GameState {
|
|||
autoCleanUses: 0,
|
||||
...defaultSounds(),
|
||||
rerolls: 0,
|
||||
loop:0
|
||||
loop: 0,
|
||||
baseCombo: 1,
|
||||
};
|
||||
resetBalls(gameState);
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {GameState} from "./types";
|
||||
import {icons} from "./loadGameData";
|
||||
import {t} from "./i18n/i18n";
|
||||
import {getSettingValue, setSettingValue} from "./settings";
|
||||
import {asyncAlert} from "./asyncAlert";
|
||||
import {openMainMenu} from "./game";
|
||||
import { GameState } from "./types";
|
||||
import { icons } from "./loadGameData";
|
||||
import { t } from "./i18n/i18n";
|
||||
import { getSettingValue, setSettingValue } from "./settings";
|
||||
import { asyncAlert } from "./asyncAlert";
|
||||
import { openMainMenu } from "./game";
|
||||
|
||||
const publicKeyString = `-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q
|
||||
|
@ -94,32 +94,32 @@ export function premiumMenuEntry(gameState: GameState) {
|
|||
text: t("premium.thanks"),
|
||||
help: t("premium.thanks_help"),
|
||||
value: async () => {
|
||||
navigator.clipboard.writeText(getSettingValue('license', ''))
|
||||
openMainMenu()
|
||||
navigator.clipboard.writeText(getSettingValue("license", ""));
|
||||
openMainMenu();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let text = t("premium.title")
|
||||
let help = t("premium.buy")
|
||||
let text = t("premium.title");
|
||||
let help = t("premium.buy");
|
||||
try {
|
||||
const timePlayed = localStorage.getItem('breakout_71_total_play_time')
|
||||
const timePlayed = localStorage.getItem("breakout_71_total_play_time");
|
||||
if (timePlayed && !isGooglePlayInstall) {
|
||||
const hours = parseFloat(timePlayed) / 1000 / 60 / 60
|
||||
const pricePerHours = 4.99 / hours
|
||||
const hours = parseFloat(timePlayed) / 1000 / 60 / 60;
|
||||
const pricePerHours = 4.99 / hours;
|
||||
const args = {
|
||||
hours: Math.floor(hours),
|
||||
pricePerHours: pricePerHours.toFixed(2)
|
||||
}
|
||||
pricePerHours: pricePerHours.toFixed(2),
|
||||
};
|
||||
if (pricePerHours > 0 && pricePerHours < 0.5) {
|
||||
text = t("premium.per_hours", args)
|
||||
help = t("premium.per_hours_help", args)
|
||||
text = t("premium.per_hours", args);
|
||||
help = t("premium.per_hours_help", args);
|
||||
}
|
||||
|
||||
console.log({args})
|
||||
console.log({ args });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -131,12 +131,9 @@ export function premiumMenuEntry(gameState: GameState) {
|
|||
}
|
||||
|
||||
const isGooglePlayInstall =
|
||||
new URLSearchParams(location.search).get("source") ===
|
||||
"com.android.vending";
|
||||
new URLSearchParams(location.search).get("source") === "com.android.vending";
|
||||
|
||||
async function openPremiumMenu(text) {
|
||||
|
||||
|
||||
const cb = await asyncAlert({
|
||||
title: t("premium.title"),
|
||||
content: [
|
||||
|
@ -160,11 +157,11 @@ async function openPremiumMenu(text) {
|
|||
text: t("premium.enter"),
|
||||
help: t("premium.enter_help"),
|
||||
async value() {
|
||||
const value = (prompt("Please paste your license key") || "").replace(
|
||||
/\s+/g,
|
||||
"",
|
||||
);
|
||||
const problem = await checkKey(value);
|
||||
const value = (
|
||||
prompt("Please paste your license key") || ""
|
||||
)?.replace(/\s+/g, "");
|
||||
|
||||
const problem = await checkKey(value || "");
|
||||
if (problem) {
|
||||
openPremiumMenu(problem).then();
|
||||
} else {
|
||||
|
|
|
@ -52,9 +52,7 @@ export function drawMainCanvasOnSmallCanvas(gameState: GameState) {
|
|||
|
||||
recordCanvasCtx.textAlign = "left";
|
||||
recordCanvasCtx.fillText(
|
||||
"Level " +
|
||||
(gameState.currentLevel + 1) +
|
||||
"/" + max_levels(gameState),
|
||||
"Level " + (gameState.currentLevel + 1) + "/" + max_levels(gameState),
|
||||
12,
|
||||
12,
|
||||
);
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import {baseCombo, forEachLiveOne, interferenceFactor, liveCount} from "./gameStateMutators";
|
||||
import {
|
||||
baseCombo,
|
||||
forEachLiveOne,
|
||||
interferenceFactor,
|
||||
liveCount,
|
||||
} from "./gameStateMutators";
|
||||
import {
|
||||
brickCenterX,
|
||||
brickCenterY,
|
||||
|
@ -9,10 +14,10 @@ import {
|
|||
isYoyoActive,
|
||||
max_levels,
|
||||
} from "./game_utils";
|
||||
import {colorString, GameState} from "./types";
|
||||
import {t} from "./i18n/i18n";
|
||||
import {gameState} from "./game";
|
||||
import {isOptionOn} from "./options";
|
||||
import { colorString, GameState } from "./types";
|
||||
import { t } from "./i18n/i18n";
|
||||
import { gameState } from "./game";
|
||||
import { isOptionOn } from "./options";
|
||||
|
||||
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
|
||||
export const ctx = gameCanvas.getContext("2d", {
|
||||
|
@ -32,15 +37,17 @@ export function render(gameState: GameState) {
|
|||
const level = currentLevelInfo(gameState);
|
||||
|
||||
const hasCombo = gameState.combo > baseCombo(gameState);
|
||||
const {width, height} = gameCanvas;
|
||||
const { width, height } = gameCanvas;
|
||||
if (!width || !height) return;
|
||||
|
||||
if (gameState.currentLevel || gameState.levelTime) {
|
||||
menuLabel.innerText = gameState.loop ? t("play.current_lvl_loop", {
|
||||
menuLabel.innerText = gameState.loop
|
||||
? t("play.current_lvl_loop", {
|
||||
level: gameState.currentLevel + 1,
|
||||
max: max_levels(gameState),
|
||||
loop: gameState.loop
|
||||
}) : t("play.current_lvl", {
|
||||
loop: gameState.loop,
|
||||
})
|
||||
: t("play.current_lvl", {
|
||||
level: gameState.currentLevel + 1,
|
||||
max: max_levels(gameState),
|
||||
});
|
||||
|
@ -91,7 +98,7 @@ export function render(gameState: GameState) {
|
|||
ctx.globalAlpha = 1;
|
||||
|
||||
forEachLiveOne(gameState.particles, (flash) => {
|
||||
const {x, y, time, color, size, duration} = flash;
|
||||
const { x, y, time, color, size, duration } = flash;
|
||||
const elapsed = gameState.levelTime - time;
|
||||
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
|
||||
drawFuzzyBall(ctx, color, size * 3, x, y);
|
||||
|
@ -134,7 +141,7 @@ export function render(gameState: GameState) {
|
|||
ctx.fillStyle = level.color || "#000";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
forEachLiveOne(gameState.particles, (flash) => {
|
||||
const {x, y, time, color, size, duration} = flash;
|
||||
const { x, y, time, color, size, duration } = flash;
|
||||
const elapsed = gameState.levelTime - time;
|
||||
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
|
||||
drawBall(ctx, color, size, x, y);
|
||||
|
@ -162,7 +169,7 @@ export function render(gameState: GameState) {
|
|||
// Coins
|
||||
ctx.globalAlpha = 1;
|
||||
forEachLiveOne(gameState.coins, (coin) => {
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
// ctx.globalCompositeOperation =
|
||||
// coin.color === "gold" || level.color ? "source-over" : "screen";
|
||||
drawCoin(
|
||||
|
@ -195,52 +202,43 @@ export function render(gameState: GameState) {
|
|||
|
||||
if (gameState.debuffs.negative_coins) {
|
||||
// Render crimson coins very bright
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
ctx.globalAlpha = 0.8
|
||||
const red = Math.floor(gameState.levelTime / 100) % 2 > 0
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
ctx.globalAlpha = 0.8;
|
||||
const red = Math.floor(gameState.levelTime / 100) % 2 > 0;
|
||||
forEachLiveOne(gameState.coins, (coin) => {
|
||||
if (coin.color !== 'crimson') return
|
||||
drawBall(
|
||||
ctx,
|
||||
red ? 'red' : 'black',
|
||||
coin.size * 3,
|
||||
coin.x,
|
||||
coin.y
|
||||
);
|
||||
if (coin.color !== "crimson") return;
|
||||
drawBall(ctx, red ? "red" : "black", coin.size * 3, coin.x, coin.y);
|
||||
});
|
||||
ctx.globalAlpha = 1
|
||||
ctx.globalAlpha = 1;
|
||||
forEachLiveOne(gameState.coins, (coin) => {
|
||||
if (coin.color !== 'crimson') return
|
||||
if (coin.color !== "crimson") return;
|
||||
drawCoin(
|
||||
ctx,
|
||||
!red ? 'red' : 'black',
|
||||
!red ? "red" : "black",
|
||||
coin.size,
|
||||
coin.x,
|
||||
coin.y,
|
||||
'red',
|
||||
coin.a
|
||||
"red",
|
||||
coin.a,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
renderAllBricks();
|
||||
|
||||
|
||||
ctx.globalCompositeOperation = "screen";
|
||||
forEachLiveOne(gameState.lights, (flash) => {
|
||||
const {x, y, time, color, size, duration} = flash;
|
||||
const { x, y, time, color, size, duration } = flash;
|
||||
const elapsed = gameState.levelTime - time;
|
||||
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5;
|
||||
drawBrick(ctx, color, x, y, -1)
|
||||
drawBrick(ctx, color, x, y, -1);
|
||||
});
|
||||
|
||||
ctx.globalCompositeOperation = "screen";
|
||||
forEachLiveOne(gameState.texts, (flash) => {
|
||||
const {x, y, time, color, size, duration} = flash;
|
||||
const { x, y, time, color, size, duration } = flash;
|
||||
const elapsed = gameState.levelTime - time;
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
|
@ -248,7 +246,7 @@ export function render(gameState: GameState) {
|
|||
});
|
||||
|
||||
forEachLiveOne(gameState.particles, (particle) => {
|
||||
const {x, y, time, color, size, duration} = particle;
|
||||
const { x, y, time, color, size, duration } = particle;
|
||||
const elapsed = gameState.levelTime - time;
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
|
||||
ctx.globalCompositeOperation = "screen";
|
||||
|
@ -289,15 +287,13 @@ export function render(gameState: GameState) {
|
|||
);
|
||||
|
||||
if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) {
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
|
||||
if (interferenceFactor(gameState) == -1) {
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = 'red'
|
||||
ctx.setLineDash(redBorderDash)
|
||||
ctx.lineDashOffset = getDashOffset(gameState)
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = "red";
|
||||
ctx.setLineDash(redBorderDash);
|
||||
ctx.lineDashOffset = getDashOffset(gameState);
|
||||
} else {
|
||||
ctx.strokeStyle = gameState.puckColor;
|
||||
}
|
||||
|
@ -311,8 +307,8 @@ export function render(gameState: GameState) {
|
|||
);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.lineWidth = 2
|
||||
ctx.setLineDash(emptyArray)
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash(emptyArray);
|
||||
}
|
||||
if (gameState.perks.clairvoyant && gameState.ballStickToPuck) {
|
||||
ctx.strokeStyle = gameState.ballsColor;
|
||||
|
|
12
src/types.d.ts
vendored
12
src/types.d.ts
vendored
|
@ -83,6 +83,7 @@ export type Coin = {
|
|||
sa: number;
|
||||
weight: number;
|
||||
destroyed?: boolean;
|
||||
collidedLastFrame?: boolean;
|
||||
coloredABrick?: boolean;
|
||||
};
|
||||
export type Ball = {
|
||||
|
@ -155,12 +156,12 @@ export type PerksMap = {
|
|||
[k in PerkId]: number;
|
||||
};
|
||||
|
||||
type Debuff={
|
||||
type Debuff = {
|
||||
id: DebuffId;
|
||||
max:number;
|
||||
name:(lvl: number,banned:string)=>string;
|
||||
help:(lvl: number,perk:string)=>string;
|
||||
}
|
||||
max: number;
|
||||
name: (lvl: number, banned: string) => string;
|
||||
help: (lvl: number, perk: string) => string;
|
||||
};
|
||||
export type DebuffId = (typeof debuffs)[number]["id"];
|
||||
|
||||
export type DebuffsMap = {
|
||||
|
@ -287,6 +288,7 @@ export type GameState = {
|
|||
};
|
||||
rerolls: number;
|
||||
loop: number;
|
||||
baseCombo: number;
|
||||
};
|
||||
|
||||
export type RunParams = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue