mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-20 12:15:06 -04:00
Typed existing game.ts
This commit is contained in:
parent
3cb662bc92
commit
6850d3b652
6 changed files with 632 additions and 444 deletions
68
Readme.md
68
Readme.md
|
@ -86,8 +86,7 @@ play is intended or if it should even be allowed.
|
||||||
- restart run on r
|
- restart run on r
|
||||||
- when missing, redo particle trail, but give speed to particle that matches ball direction
|
- when missing, redo particle trail, but give speed to particle that matches ball direction
|
||||||
|
|
||||||
# Perks ideas
|
# New perks ideas
|
||||||
- Combo balancing : make Compound Interest less OP by defaulting to soft reset for others, or by making it loose more for each missed coin
|
|
||||||
- second puck (symmeric to the first one)
|
- second puck (symmeric to the first one)
|
||||||
- keep combo between level, loose half your run score when missing any bricks
|
- keep combo between level, loose half your run score when missing any bricks
|
||||||
- offer next level choice after upgrade pick
|
- offer next level choice after upgrade pick
|
||||||
|
@ -130,6 +129,21 @@ play is intended or if it should even be allowed.
|
||||||
- correction : pick one past upgrade to remove and replace by something else
|
- correction : pick one past upgrade to remove and replace by something else
|
||||||
- puck bounce predictions rendered with particles or lines (requires big refactor)
|
- puck bounce predictions rendered with particles or lines (requires big refactor)
|
||||||
- the more balls are close to a brick, the more coins she spawns when breaking
|
- the more balls are close to a brick, the more coins she spawns when breaking
|
||||||
|
- combo resets when puck moves
|
||||||
|
- gravity is flipped on the opposite side to the puck (for coins)
|
||||||
|
- balls have gravity
|
||||||
|
- coins don't have gravity
|
||||||
|
|
||||||
|
# Balancing ideas
|
||||||
|
|
||||||
|
The dominant strategy now is Compound Interest lvl 3 + coin magnet/viscosity/
|
||||||
|
and hot start + explosives and multiball
|
||||||
|
|
||||||
|
- make Compound Interest less OP making it full reset when coins lost
|
||||||
|
- cap hot start to lvl 2, or make it decrease faster
|
||||||
|
- make puck smaller as combo increases ?
|
||||||
|
- coin magnet has no effect when too close, or coins might overshoot, or coins bounce and spread more ?
|
||||||
|
- add red anti-coins, they destroy your combo and your score, and they behave like heavier coins.
|
||||||
|
|
||||||
# extra levels
|
# extra levels
|
||||||
|
|
||||||
|
@ -156,3 +170,53 @@ and let them spend those coins on upgrades. The upgrades would then be explained
|
||||||
"reach high score of 1000" or 'reach high score of 99999 without using "hot start"'.
|
"reach high score of 1000" or 'reach high score of 99999 without using "hot start"'.
|
||||||
This requires recording a bit more info about each run.
|
This requires recording a bit more info about each run.
|
||||||
I could unlock the "pro stand" at $999 that just holds the play area higher.
|
I could unlock the "pro stand" at $999 that just holds the play area higher.
|
||||||
|
|
||||||
|
|
||||||
|
# Colin's feedback (cwpute/obigre)
|
||||||
|
|
||||||
|
Perks:
|
||||||
|
|
||||||
|
* yoyo - when the ball falls back down, it curbs towards your puck (after hitting a brick or top)
|
||||||
|
* single block combo - get +1 combo if the ball only breaks a single block before reaching the puck
|
||||||
|
* mirror puck - a mirrored puck at the top of the screen follows as you move the bottom puck. it helps with keeping combos up and preventing the ball from touching the ceiling. it could appear as a hollow puck so as to not draw too much attention from the main bottom puck.
|
||||||
|
* side pucks - same as above but with two side pucks.
|
||||||
|
* ball coins - coins share the same physics as coins and bounce on walls and bricks
|
||||||
|
* phantom coins - coins pass through bricks
|
||||||
|
* drifting coins - coins slowly drift away from the brick they were generated from, and they need to be collected by the ball
|
||||||
|
* bigger ball - self-explanatory
|
||||||
|
* smaller ball - yes.
|
||||||
|
* sturdy ball - does more damage to bricks, to conter sturdy bricks
|
||||||
|
* accumulation - coins aglutinate into bigger coins that hold more value
|
||||||
|
* forgiving - you can miss several times without losing your combo. or alternatively, include this ability into the soft reset perk.
|
||||||
|
* plot - plot the ball's trajectory as you position your puck
|
||||||
|
* golden corners - catch coins at the sides of the puck to double their value
|
||||||
|
* varied diet - your combo grows if you keep hitting different coloured bricks each time
|
||||||
|
* earthquake - when the puck hits any side of the screen with velocity, the screen shakes and a brick explodes/falls from the level. alternatively, any brick you catch with the puck gives you the coins at the current combo rate. each level lowers the amount of hits before a brick falls
|
||||||
|
* statue - stand still to make the combo grow. move for too long and thi combo will quickly drop
|
||||||
|
* piggy bank - bricks absorb coins that fall onto it, and release them back as they are broken, with added value
|
||||||
|
* trickle up - if you first hit is the lowest brick of a column, all bricks above get +1 coin inside
|
||||||
|
* wormhole - the puck sometimes don't bounce the ball back up but teleports it to the top of the screen as if it fell through from bottom to top. higher levels reduce the times it takes to reload that effect
|
||||||
|
* hitman - hit the marked brick for +5 combo. each level increases the combo you get for it.
|
||||||
|
* sweet spot - place your puck directly below a moving spot at the top of the level to increase your combo
|
||||||
|
|
||||||
|
IMPROVEMENTS ON EXISTING PERKS :
|
||||||
|
|
||||||
|
* separate the "shoot straight" perk into two : one for left-side, the other for right-side. it will help alleviate the high difficulty of this challenge and provide more interesting ways to play around it. the wind perk could even find a use.
|
||||||
|
* wind perk is fun but very much unusable. i do not see any situation it can help with. i favor "puck control ball" anytime over it. maybe it blew less hard it could be played with. maybe reuse its mechanic as a level hazard.
|
||||||
|
* soft-landing is only interesting starting level 2, and only in synergy with "single-puck hit streak"
|
||||||
|
* instead of "lives", have the perk be like a fourth wall that prevents the ball from falling down but disapears after one strike. it is functionally the same but provides visual feedback to the player so they know they have that perk.
|
||||||
|
* limit levels to only a handful of coulours, like 5 max, so that the colour-related perks are more viable.
|
||||||
|
|
||||||
|
GENERAL REMARKS ON DIFFERENT ASPECTS :
|
||||||
|
|
||||||
|
* when the player reaches the last level, alow them to loop the run, unlocking a permanent bonus for this run. For example: +5 combo, +1 life per loop… the counterpart would be hazards that slowly populate the levels.
|
||||||
|
* different visual effects on ball to represent which perks it's imbued with (pierce, sapper…). remove visual while it's not affected (can't pierce/sap anymore until touching the puck).
|
||||||
|
* always visually put the ball on top of coins so as to clearly see it. sometimes a black outline appears to distinguesh it from coins, this should be used more often imo.
|
||||||
|
* not brick-shaped bricks, or tilted bricks, that can bounce the ball into fun angles to spice up the game. or even moving blocks !
|
||||||
|
* reward the player with more choices/perks for breaking a brick while having reached an increasing combo thresholds. 5 combo, then 10, then 20, then 40 etc… once a threshold is reached you aren't rewarded for that threshold again until you start a rew run
|
||||||
|
* inspired by Balatro's score system : have some perks add to the multiplicator, and some others to the amount of coins in a brick (or the raw value of coins inside), so that you users want to improve both for maximized profit ! maybe tie one of the to perks that help you, and the other to perks that are bad to you, so that gambling players are forced to make their life harder
|
||||||
|
* the white outline on bricks asociated with picky eater kinda works but i feel it's more distracting than anything. maybe try something different ? put a cross on matching coloured bricks, or the contrary, grey out other bricks.
|
||||||
|
* also regarding colour : make it so the ball always start with a colour that matches one currently present in the level. sometimes you don't have white present and it's a waste of a combo :/
|
||||||
|
* negative coins, they would spawn from bricks as a hazard and do any of the following: -deactivate a perk for this level -reduce your number of coins -reduce your choice for your next perk -despawn all current coins on screen -lowers your combo. they could either be a negative perk with a bonus, like the small puck, or a hazard that spawns in later levels.
|
||||||
|
* the way combos look on the puck was better when you didn't see the coin visual on it ! now it easily overflows out of the puck with reduced visibility
|
||||||
|
|
||||||
|
|
336
dist/index.html
vendored
336
dist/index.html
vendored
|
@ -920,13 +920,21 @@ function hmrAccept(bundle /*: ParcelRequire */ , id /*: string */ ) {
|
||||||
var _gameTs = require("./game.ts");
|
var _gameTs = require("./game.ts");
|
||||||
|
|
||||||
},{"./game.ts":"edeGs"}],"edeGs":[function(require,module,exports,__globalThis) {
|
},{"./game.ts":"edeGs"}],"edeGs":[function(require,module,exports,__globalThis) {
|
||||||
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
||||||
|
parcelHelpers.defineInteropFlag(exports);
|
||||||
|
parcelHelpers.export(exports, "gameCanvas", ()=>gameCanvas);
|
||||||
|
parcelHelpers.export(exports, "fitSize", ()=>fitSize);
|
||||||
|
parcelHelpers.export(exports, "isSettingOn", ()=>isSettingOn);
|
||||||
|
parcelHelpers.export(exports, "toggleSetting", ()=>toggleSetting);
|
||||||
var _loadGameData = require("./loadGameData");
|
var _loadGameData = require("./loadGameData");
|
||||||
|
var _options = require("./options");
|
||||||
const MAX_COINS = 400;
|
const MAX_COINS = 400;
|
||||||
const MAX_PARTICLES = 600;
|
const MAX_PARTICLES = 600;
|
||||||
const canvas = document.getElementById("game");
|
const gameCanvas = document.getElementById("game");
|
||||||
let ctx = canvas.getContext("2d", {
|
let ctx = gameCanvas.getContext("2d", {
|
||||||
alpha: false
|
alpha: false
|
||||||
});
|
});
|
||||||
|
const puckColor = "#FFF";
|
||||||
let ballSize = 20;
|
let ballSize = 20;
|
||||||
const coinSize = Math.round(ballSize * 0.8);
|
const coinSize = Math.round(ballSize * 0.8);
|
||||||
const puckHeight = ballSize;
|
const puckHeight = ballSize;
|
||||||
|
@ -992,8 +1000,9 @@ let running = false, puck = 400, pauseTimeout = null;
|
||||||
function play() {
|
function play() {
|
||||||
if (running) return;
|
if (running) return;
|
||||||
running = true;
|
running = true;
|
||||||
if (audioContext) audioContext.resume();
|
if (audioContext) audioContext.resume().then();
|
||||||
resumeRecording();
|
resumeRecording();
|
||||||
|
document.body.className = running ? " running " : " paused ";
|
||||||
}
|
}
|
||||||
function pause(playerAskedForPause) {
|
function pause(playerAskedForPause) {
|
||||||
if (!running) return;
|
if (!running) return;
|
||||||
|
@ -1002,10 +1011,11 @@ function pause(playerAskedForPause) {
|
||||||
running = false;
|
running = false;
|
||||||
needsRender = true;
|
needsRender = true;
|
||||||
if (audioContext) setTimeout(()=>{
|
if (audioContext) setTimeout(()=>{
|
||||||
if (!running) audioContext.suspend();
|
if (!running) audioContext.suspend().then();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
pauseRecording();
|
pauseRecording();
|
||||||
pauseTimeout = null;
|
pauseTimeout = null;
|
||||||
|
document.body.className = running ? " running " : " paused ";
|
||||||
}, Math.min(Math.max(0, pauseUsesDuringRun - 5) * 50, 500));
|
}, Math.min(Math.max(0, pauseUsesDuringRun - 5) * 50, 500));
|
||||||
if (playerAskedForPause) // Pausing many times in a run will make pause slower
|
if (playerAskedForPause) // Pausing many times in a run will make pause slower
|
||||||
pauseUsesDuringRun++;
|
pauseUsesDuringRun++;
|
||||||
|
@ -1018,19 +1028,19 @@ background.addEventListener("load", ()=>{
|
||||||
needsRender = true;
|
needsRender = true;
|
||||||
});
|
});
|
||||||
const fitSize = ()=>{
|
const fitSize = ()=>{
|
||||||
const { width, height } = canvas.getBoundingClientRect();
|
const { width, height } = gameCanvas.getBoundingClientRect();
|
||||||
canvas.width = width;
|
gameCanvas.width = width;
|
||||||
canvas.height = height;
|
gameCanvas.height = height;
|
||||||
ctx.fillStyle = currentLevelInfo()?.color || "black";
|
ctx.fillStyle = currentLevelInfo()?.color || "black";
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
backgroundCanvas.width = width;
|
backgroundCanvas.width = width;
|
||||||
backgroundCanvas.height = height;
|
backgroundCanvas.height = height;
|
||||||
gameZoneHeight = isSettingOn("mobile-mode") ? height * 80 / 100 : height;
|
gameZoneHeight = isSettingOn("mobile-mode") ? height * 80 / 100 : height;
|
||||||
const baseWidth = Math.round(Math.min(canvas.width, gameZoneHeight * 0.73));
|
const baseWidth = Math.round(Math.min(gameCanvas.width, gameZoneHeight * 0.73));
|
||||||
brickWidth = Math.floor(baseWidth / gridSize / 2) * 2;
|
brickWidth = Math.floor(baseWidth / gridSize / 2) * 2;
|
||||||
gameZoneWidth = brickWidth * gridSize;
|
gameZoneWidth = brickWidth * gridSize;
|
||||||
offsetX = Math.floor((canvas.width - gameZoneWidth) / 2);
|
offsetX = Math.floor((gameCanvas.width - gameZoneWidth) / 2);
|
||||||
offsetXRoundedDown = offsetX;
|
offsetXRoundedDown = offsetX;
|
||||||
if (offsetX < ballSize) offsetXRoundedDown = 0;
|
if (offsetX < ballSize) offsetXRoundedDown = 0;
|
||||||
gameZoneWidthRoundedUp = width - 2 * offsetXRoundedDown;
|
gameZoneWidthRoundedUp = width - 2 * offsetXRoundedDown;
|
||||||
|
@ -1096,7 +1106,7 @@ function addToScore(coin) {
|
||||||
color: coin.color,
|
color: coin.color,
|
||||||
x: coin.previousx,
|
x: coin.previousx,
|
||||||
y: coin.previousy,
|
y: coin.previousy,
|
||||||
vx: (canvas.width - coin.x) / 100,
|
vx: (gameCanvas.width - coin.x) / 100,
|
||||||
vy: -coin.y / 100,
|
vy: -coin.y / 100,
|
||||||
ethereal: true
|
ethereal: true
|
||||||
});
|
});
|
||||||
|
@ -1254,7 +1264,7 @@ function currentLevelInfo() {
|
||||||
function reset_perks() {
|
function reset_perks() {
|
||||||
for (let u of (0, _loadGameData.upgrades))perks[u.id] = 0;
|
for (let u of (0, _loadGameData.upgrades))perks[u.id] = 0;
|
||||||
const giftable = getPossibleUpgrades().filter((u)=>u.giftable);
|
const giftable = getPossibleUpgrades().filter((u)=>u.giftable);
|
||||||
const randomGift = nextRunOverrides?.perk || isSettingOn("easy") ? "slow_down" : giftable[Math.floor(Math.random() * giftable.length)].id;
|
const randomGift = nextRunOverrides?.perk || isSettingOn("easy") && "slow_down" || giftable[Math.floor(Math.random() * giftable.length)].id;
|
||||||
perks[randomGift] = 1;
|
perks[randomGift] = 1;
|
||||||
delete nextRunOverrides.perk;
|
delete nextRunOverrides.perk;
|
||||||
return randomGift;
|
return randomGift;
|
||||||
|
@ -1328,34 +1338,34 @@ function setMousePos(x) {
|
||||||
if (puck > offsetXRoundedDown + gameZoneWidthRoundedUp - puckWidth / 2) puck = offsetXRoundedDown + gameZoneWidthRoundedUp - puckWidth / 2;
|
if (puck > offsetXRoundedDown + gameZoneWidthRoundedUp - puckWidth / 2) puck = offsetXRoundedDown + gameZoneWidthRoundedUp - puckWidth / 2;
|
||||||
if (!running && !levelTime) putBallsAtPuck();
|
if (!running && !levelTime) putBallsAtPuck();
|
||||||
}
|
}
|
||||||
canvas.addEventListener("mouseup", (e)=>{
|
gameCanvas.addEventListener("mouseup", (e)=>{
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
if (running) pause(true);
|
if (running) pause(true);
|
||||||
else {
|
else {
|
||||||
play();
|
play();
|
||||||
if (isSettingOn("pointerLock")) canvas.requestPointerLock();
|
if (isSettingOn("pointerLock")) gameCanvas.requestPointerLock();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
canvas.addEventListener("mousemove", (e)=>{
|
gameCanvas.addEventListener("mousemove", (e)=>{
|
||||||
if (document.pointerLockElement === canvas) setMousePos(puck + e.movementX);
|
if (document.pointerLockElement === gameCanvas) setMousePos(puck + e.movementX);
|
||||||
else setMousePos(e.x);
|
else setMousePos(e.x);
|
||||||
});
|
});
|
||||||
canvas.addEventListener("touchstart", (e)=>{
|
gameCanvas.addEventListener("touchstart", (e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!e.touches?.length) return;
|
if (!e.touches?.length) return;
|
||||||
setMousePos(e.touches[0].pageX);
|
setMousePos(e.touches[0].pageX);
|
||||||
play();
|
play();
|
||||||
});
|
});
|
||||||
canvas.addEventListener("touchend", (e)=>{
|
gameCanvas.addEventListener("touchend", (e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
pause(true);
|
pause(true);
|
||||||
});
|
});
|
||||||
canvas.addEventListener("touchcancel", (e)=>{
|
gameCanvas.addEventListener("touchcancel", (e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
pause(true);
|
pause(true);
|
||||||
needsRender = true;
|
needsRender = true;
|
||||||
});
|
});
|
||||||
canvas.addEventListener("touchmove", (e)=>{
|
gameCanvas.addEventListener("touchmove", (e)=>{
|
||||||
if (!e.touches?.length) return;
|
if (!e.touches?.length) return;
|
||||||
setMousePos(e.touches[0].pageX);
|
setMousePos(e.touches[0].pageX);
|
||||||
});
|
});
|
||||||
|
@ -1376,36 +1386,58 @@ function shouldPierceByColor(vhit, hhit, chit) {
|
||||||
if (typeof chit !== "undefined" && bricks[chit] !== ballsColor) return false;
|
if (typeof chit !== "undefined" && bricks[chit] !== ballsColor) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function brickHitCheck(ballOrCoin, radius, isBall) {
|
function ballBrickHitCheck(ball) {
|
||||||
|
const radius = ballSize / 2;
|
||||||
// Make ball/coin bonce, and return bricks that were hit
|
// Make ball/coin bonce, and return bricks that were hit
|
||||||
const { x, y, previousx, previousy } = ballOrCoin;
|
const { x, y, previousx, previousy } = ball;
|
||||||
const vhit = hitsSomething(previousx, y, radius);
|
const vhit = hitsSomething(previousx, y, radius);
|
||||||
const hhit = hitsSomething(x, previousy, radius);
|
const hhit = hitsSomething(x, previousy, radius);
|
||||||
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && hitsSomething(x, y, radius) || undefined;
|
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && hitsSomething(x, y, radius) || undefined;
|
||||||
let pierce = isBall && ballOrCoin.piercedSinceBounce < perks.pierce * 3;
|
let pierce = ball.piercedSinceBounce < perks.pierce * 3;
|
||||||
if (pierce && (typeof vhit !== "undefined" || typeof hhit !== "undefined" || typeof chit !== "undefined")) ballOrCoin.piercedSinceBounce++;
|
if (pierce && (typeof vhit !== "undefined" || typeof hhit !== "undefined" || typeof chit !== "undefined")) ball.piercedSinceBounce++;
|
||||||
if (isBall && shouldPierceByColor(vhit, hhit, chit)) pierce = true;
|
if (shouldPierceByColor(vhit, hhit, chit)) pierce = true;
|
||||||
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
|
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
|
||||||
if (!pierce) {
|
if (!pierce) {
|
||||||
ballOrCoin.y = ballOrCoin.previousy;
|
ball.y = ball.previousy;
|
||||||
ballOrCoin.vy *= -1;
|
ball.vy *= -1;
|
||||||
}
|
|
||||||
if (!isBall) {
|
|
||||||
// Roll on corners
|
|
||||||
const leftHit = bricks[brickIndex(x - radius, y + radius)];
|
|
||||||
const rightHit = bricks[brickIndex(x + radius, y + radius)];
|
|
||||||
if (leftHit && !rightHit) ballOrCoin.vx += 1;
|
|
||||||
if (!leftHit && rightHit) ballOrCoin.vx -= 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
|
if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
|
||||||
if (!pierce) {
|
if (!pierce) {
|
||||||
ballOrCoin.x = ballOrCoin.previousx;
|
ball.x = ball.previousx;
|
||||||
ballOrCoin.vx *= -1;
|
ball.vx *= -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vhit ?? hhit ?? chit;
|
return vhit ?? hhit ?? chit;
|
||||||
}
|
}
|
||||||
|
function coinBrickHitCheck(coin) {
|
||||||
|
// Make ball/coin bonce, and return bricks that were hit
|
||||||
|
const radius = coinSize / 2;
|
||||||
|
const { x, y, previousx, previousy } = coin;
|
||||||
|
const vhit = hitsSomething(previousx, y, radius);
|
||||||
|
const hhit = hitsSomething(x, previousy, radius);
|
||||||
|
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && hitsSomething(x, y, radius) || undefined;
|
||||||
|
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
|
||||||
|
coin.y = coin.previousy;
|
||||||
|
coin.vy *= -1;
|
||||||
|
// Roll on corners
|
||||||
|
const leftHit = bricks[brickIndex(x - radius, y + radius)];
|
||||||
|
const rightHit = bricks[brickIndex(x + radius, y + radius)];
|
||||||
|
if (leftHit && !rightHit) {
|
||||||
|
coin.vx += 1;
|
||||||
|
coin.sa -= 1;
|
||||||
|
}
|
||||||
|
if (!leftHit && rightHit) {
|
||||||
|
coin.vx -= 1;
|
||||||
|
coin.sa += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
|
||||||
|
coin.x = coin.previousx;
|
||||||
|
coin.vx *= -1;
|
||||||
|
}
|
||||||
|
return vhit ?? hhit ?? chit;
|
||||||
|
}
|
||||||
function bordersHitCheck(coin, radius, delta) {
|
function bordersHitCheck(coin, radius, delta) {
|
||||||
if (coin.destroyed) return;
|
if (coin.destroyed) return;
|
||||||
coin.previousx = coin.x;
|
coin.previousx = coin.x;
|
||||||
|
@ -1430,8 +1462,8 @@ function bordersHitCheck(coin, radius, delta) {
|
||||||
coin.vy *= -1;
|
coin.vy *= -1;
|
||||||
vhit = 1;
|
vhit = 1;
|
||||||
}
|
}
|
||||||
if (coin.x > canvas.width - offsetXRoundedDown - radius) {
|
if (coin.x > gameCanvas.width - offsetXRoundedDown - radius) {
|
||||||
coin.x = canvas.width - offsetXRoundedDown - radius;
|
coin.x = gameCanvas.width - offsetXRoundedDown - radius;
|
||||||
coin.vx *= -1;
|
coin.vx *= -1;
|
||||||
hhit = 1;
|
hhit = 1;
|
||||||
}
|
}
|
||||||
|
@ -1483,11 +1515,11 @@ function tick() {
|
||||||
const hitBorder = bordersHitCheck(coin, coinRadius, delta);
|
const hitBorder = bordersHitCheck(coin, coinRadius, delta);
|
||||||
if (coin.y > gameZoneHeight - coinRadius - puckHeight && coin.y < gameZoneHeight + puckHeight + coin.vy && Math.abs(coin.x - puck) < coinRadius + puckWidth / 2 + // a bit of margin to be nice
|
if (coin.y > gameZoneHeight - coinRadius - puckHeight && coin.y < gameZoneHeight + puckHeight + coin.vy && Math.abs(coin.x - puck) < coinRadius + puckWidth / 2 + // a bit of margin to be nice
|
||||||
puckHeight) addToScore(coin);
|
puckHeight) addToScore(coin);
|
||||||
else if (coin.y > canvas.height + coinRadius) {
|
else if (coin.y > gameCanvas.height + coinRadius) {
|
||||||
coin.destroyed = true;
|
coin.destroyed = true;
|
||||||
if (perks.compound_interest) decreaseCombo(coin.points * perks.compound_interest, coin.x, canvas.height - coinRadius);
|
if (perks.compound_interest) resetCombo(coin.x, coin.y);
|
||||||
}
|
}
|
||||||
const hitBrick = brickHitCheck(coin, coinRadius, false);
|
const hitBrick = coinBrickHitCheck(coin);
|
||||||
if (perks.metamorphosis && typeof hitBrick !== "undefined") {
|
if (perks.metamorphosis && typeof hitBrick !== "undefined") {
|
||||||
if (bricks[hitBrick] && coin.color !== bricks[hitBrick] && bricks[hitBrick] !== "black" && !coin.coloredABrick) {
|
if (bricks[hitBrick] && coin.color !== bricks[hitBrick] && bricks[hitBrick] !== "black" && !coin.coloredABrick) {
|
||||||
bricks[hitBrick] = coin.color;
|
bricks[hitBrick] = coin.color;
|
||||||
|
@ -1508,7 +1540,7 @@ function tick() {
|
||||||
balls.forEach((ball)=>ballTick(ball, delta));
|
balls.forEach((ball)=>ballTick(ball, delta));
|
||||||
if (perks.wind) {
|
if (perks.wind) {
|
||||||
const windD = (puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth * 2 * perks.wind;
|
const windD = (puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth * 2 * perks.wind;
|
||||||
for(var i = 0; i < perks.wind; i++)if (Math.random() * Math.abs(windD) > 0.5) flashes.push({
|
for(let i = 0; i < perks.wind; i++)if (Math.random() * Math.abs(windD) > 0.5) flashes.push({
|
||||||
type: "particle",
|
type: "particle",
|
||||||
duration: 150,
|
duration: 150,
|
||||||
ethereal: true,
|
ethereal: true,
|
||||||
|
@ -1714,7 +1746,7 @@ function ballTick(ball, delta) {
|
||||||
} else gameOver("Game Over", "You dropped the ball after catching " + score + " coins. ");
|
} else gameOver("Game Over", "You dropped the ball after catching " + score + " coins. ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const hitBrick = brickHitCheck(ball, ballSize / 2, true);
|
const hitBrick = ballBrickHitCheck(ball);
|
||||||
if (typeof hitBrick !== "undefined") {
|
if (typeof hitBrick !== "undefined") {
|
||||||
const initialBrickColor = bricks[hitBrick];
|
const initialBrickColor = bricks[hitBrick];
|
||||||
explodeBrick(hitBrick, ball, false);
|
explodeBrick(hitBrick, ball, false);
|
||||||
|
@ -1832,28 +1864,32 @@ function gameOver(title, intro) {
|
||||||
<p>${intro}</p>
|
<p>${intro}</p>
|
||||||
${unlocksInfo}
|
${unlocksInfo}
|
||||||
`,
|
`,
|
||||||
textAfterButtons: `
|
actions: [
|
||||||
|
{
|
||||||
<div id="level-recording-container"></div>
|
value: null,
|
||||||
${getHistograms(true)}
|
text: 'Start a new run',
|
||||||
|
help: ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
textAfterButtons: `<div id="level-recording-container"></div>
|
||||||
|
${getHistograms()}
|
||||||
`
|
`
|
||||||
}).then(()=>restart());
|
}).then(()=>restart());
|
||||||
}
|
}
|
||||||
function getHistograms(saveStats) {
|
function getHistograms() {
|
||||||
let runStats = "";
|
let runStats = "";
|
||||||
try {
|
try {
|
||||||
// Stores only top 100 runs
|
// Stores only top 100 runs
|
||||||
let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]");
|
let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]");
|
||||||
runsHistory.sort((a, b)=>a.score - b.score).reverse();
|
runsHistory.sort((a, b)=>a.score - b.score).reverse();
|
||||||
runsHistory = runsHistory.slice(0, 100);
|
runsHistory = runsHistory.slice(0, 100);
|
||||||
const nonZeroPerks = {};
|
|
||||||
for(let k in perks)if (perks[k]) nonZeroPerks[k] = perks[k];
|
|
||||||
runsHistory.push({
|
runsHistory.push({
|
||||||
...runStatistics,
|
...runStatistics,
|
||||||
perks: nonZeroPerks
|
perks,
|
||||||
|
appVersion: (0, _loadGameData.appVersion)
|
||||||
});
|
});
|
||||||
// Generate some histogram
|
// Generate some histogram
|
||||||
if (saveStats) localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory, null, 2));
|
localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory, null, 2));
|
||||||
const makeHistogram = (title, getter, unit)=>{
|
const makeHistogram = (title, getter, unit)=>{
|
||||||
let values = runsHistory.map((h)=>getter(h) || 0);
|
let values = runsHistory.map((h)=>getter(h) || 0);
|
||||||
let min = Math.min(...values);
|
let min = Math.min(...values);
|
||||||
|
@ -2015,7 +2051,7 @@ function explodeBrick(index, ball, isExplosion) {
|
||||||
});
|
});
|
||||||
spawnExplosion(5 + Math.min(combo, 30), x, y, color, 150, coinSize / 2);
|
spawnExplosion(5 + Math.min(combo, 30), x, y, color, 150, coinSize / 2);
|
||||||
}
|
}
|
||||||
if (!bricks[index]) ball.hitItem?.push({
|
if (!bricks[index] && color !== 'black') ball.hitItem?.push({
|
||||||
index,
|
index,
|
||||||
color
|
color
|
||||||
});
|
});
|
||||||
|
@ -2028,7 +2064,7 @@ function render() {
|
||||||
if (!needsRender) return;
|
if (!needsRender) return;
|
||||||
needsRender = false;
|
needsRender = false;
|
||||||
const level = currentLevelInfo();
|
const level = currentLevelInfo();
|
||||||
const { width, height } = canvas;
|
const { width, height } = gameCanvas;
|
||||||
if (!width || !height) return;
|
if (!width || !height) return;
|
||||||
let scoreInfo = "";
|
let scoreInfo = "";
|
||||||
for(let i = 0; i < perks.extra_life; i++)scoreInfo += "\uD83D\uDDA4 ";
|
for(let i = 0; i < perks.extra_life; i++)scoreInfo += "\uD83D\uDDA4 ";
|
||||||
|
@ -2075,11 +2111,11 @@ function render() {
|
||||||
if (level.svg && background.width && background.complete) {
|
if (level.svg && background.width && background.complete) {
|
||||||
if (backgroundCanvas.title !== level.name) {
|
if (backgroundCanvas.title !== level.name) {
|
||||||
backgroundCanvas.title = level.name;
|
backgroundCanvas.title = level.name;
|
||||||
backgroundCanvas.width = canvas.width;
|
backgroundCanvas.width = gameCanvas.width;
|
||||||
backgroundCanvas.height = canvas.height;
|
backgroundCanvas.height = gameCanvas.height;
|
||||||
const bgctx = backgroundCanvas.getContext("2d");
|
const bgctx = backgroundCanvas.getContext("2d");
|
||||||
bgctx.fillStyle = level.color || "#000";
|
bgctx.fillStyle = level.color || "#000";
|
||||||
bgctx.fillRect(0, 0, canvas.width, canvas.height);
|
bgctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
|
||||||
bgctx.fillStyle = ctx.createPattern(background, "repeat");
|
bgctx.fillStyle = ctx.createPattern(background, "repeat");
|
||||||
bgctx.fillRect(0, 0, width, height);
|
bgctx.fillRect(0, 0, width, height);
|
||||||
}
|
}
|
||||||
|
@ -2110,7 +2146,7 @@ function render() {
|
||||||
ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude);
|
ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude);
|
||||||
}
|
}
|
||||||
ctx.globalCompositeOperation = "source-over";
|
ctx.globalCompositeOperation = "source-over";
|
||||||
renderAllBricks(ctx);
|
renderAllBricks();
|
||||||
ctx.globalCompositeOperation = "screen";
|
ctx.globalCompositeOperation = "screen";
|
||||||
flashes = flashes.filter((f)=>levelTime - f.time < f.duration && !f.destroyed);
|
flashes = flashes.filter((f)=>levelTime - f.time < f.duration && !f.destroyed);
|
||||||
flashes.forEach((flash)=>{
|
flashes.forEach((flash)=>{
|
||||||
|
@ -2141,10 +2177,9 @@ function render() {
|
||||||
}
|
}
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
ctx.globalCompositeOperation = "source-over";
|
ctx.globalCompositeOperation = "source-over";
|
||||||
const puckColor = "#FFF";
|
|
||||||
balls.forEach((ball)=>{
|
balls.forEach((ball)=>{
|
||||||
|
// The white border around is to distinguish colored balls from coins/bg
|
||||||
drawBall(ctx, ballsColor, ballSize, ball.x, ball.y, puckColor);
|
drawBall(ctx, ballsColor, ballSize, ball.x, ball.y, puckColor);
|
||||||
// effect
|
|
||||||
if (isTelekinesisActive(ball)) {
|
if (isTelekinesisActive(ball)) {
|
||||||
ctx.strokeStyle = puckColor;
|
ctx.strokeStyle = puckColor;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
@ -2164,7 +2199,7 @@ function render() {
|
||||||
const totalWidth = comboTextWidth + coinSize * 2;
|
const totalWidth = comboTextWidth + coinSize * 2;
|
||||||
const left = puck - totalWidth / 2;
|
const left = puck - totalWidth / 2;
|
||||||
if (totalWidth < puckWidth) {
|
if (totalWidth < puckWidth) {
|
||||||
drawCoin(ctx, "gold", coinSize, left + coinSize / 2, gameZoneHeight - puckHeight / 2, "#FFF", 0);
|
drawCoin(ctx, "gold", coinSize, left + coinSize / 2, gameZoneHeight - puckHeight / 2, puckColor, 0);
|
||||||
drawText(ctx, comboText, "#000", puckHeight, left + coinSize * 1.5, gameZoneHeight - puckHeight / 2, true);
|
drawText(ctx, comboText, "#000", puckHeight, left + coinSize * 1.5, gameZoneHeight - puckHeight / 2, true);
|
||||||
} else drawText(ctx, comboText, "#FFF", puckHeight, puck, gameZoneHeight - puckHeight / 2, false);
|
} else drawText(ctx, comboText, "#FFF", puckHeight, puck, gameZoneHeight - puckHeight / 2, false);
|
||||||
}
|
}
|
||||||
|
@ -2185,14 +2220,14 @@ function render() {
|
||||||
ctx.fillStyle = redBottom ? "red" : puckColor;
|
ctx.fillStyle = redBottom ? "red" : puckColor;
|
||||||
if (isSettingOn("mobile-mode")) {
|
if (isSettingOn("mobile-mode")) {
|
||||||
ctx.fillRect(offsetXRoundedDown, gameZoneHeight, gameZoneWidthRoundedUp, 1);
|
ctx.fillRect(offsetXRoundedDown, gameZoneHeight, gameZoneWidthRoundedUp, 1);
|
||||||
if (!running) drawText(ctx, "Press and hold here to play", puckColor, puckHeight, canvas.width / 2, gameZoneHeight + (canvas.height - gameZoneHeight) / 2);
|
if (!running) drawText(ctx, "Press and hold here to play", puckColor, puckHeight, gameCanvas.width / 2, gameZoneHeight + (gameCanvas.height - gameZoneHeight) / 2);
|
||||||
} else if (redBottom) ctx.fillRect(offsetXRoundedDown, gameZoneHeight - 1, gameZoneWidthRoundedUp, 1);
|
} else if (redBottom) ctx.fillRect(offsetXRoundedDown, gameZoneHeight - 1, gameZoneWidthRoundedUp, 1);
|
||||||
if (shaked) ctx.resetTransform();
|
if (shaked) ctx.resetTransform();
|
||||||
recordOneFrame();
|
recordOneFrame();
|
||||||
}
|
}
|
||||||
let cachedBricksRender = document.createElement("canvas");
|
let cachedBricksRender = document.createElement("canvas");
|
||||||
let cachedBricksRenderKey = null;
|
let cachedBricksRenderKey = null;
|
||||||
function renderAllBricks(destinationCtx) {
|
function renderAllBricks() {
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
const redBorderOnBricksWithWrongColor = combo > baseCombo() && perks.picky_eater;
|
const redBorderOnBricksWithWrongColor = combo > baseCombo() && perks.picky_eater;
|
||||||
const newKey = gameZoneWidth + "_" + bricks.join("_") + bombSVG.complete + "_" + redBorderOnBricksWithWrongColor + "_" + ballsColor;
|
const newKey = gameZoneWidth + "_" + bricks.join("_") + bombSVG.complete + "_" + redBorderOnBricksWithWrongColor + "_" + ballsColor;
|
||||||
|
@ -2200,24 +2235,23 @@ function renderAllBricks(destinationCtx) {
|
||||||
cachedBricksRenderKey = newKey;
|
cachedBricksRenderKey = newKey;
|
||||||
cachedBricksRender.width = gameZoneWidth;
|
cachedBricksRender.width = gameZoneWidth;
|
||||||
cachedBricksRender.height = gameZoneWidth + 1;
|
cachedBricksRender.height = gameZoneWidth + 1;
|
||||||
const ctx = cachedBricksRender.getContext("2d");
|
const canctx = cachedBricksRender.getContext("2d");
|
||||||
ctx.clearRect(0, 0, gameZoneWidth, gameZoneWidth);
|
canctx.clearRect(0, 0, gameZoneWidth, gameZoneWidth);
|
||||||
ctx.resetTransform();
|
canctx.resetTransform();
|
||||||
ctx.translate(-offsetX, 0);
|
canctx.translate(-offsetX, 0);
|
||||||
// Bricks
|
// Bricks
|
||||||
const puckColor = "#FFF";
|
|
||||||
bricks.forEach((color, index)=>{
|
bricks.forEach((color, index)=>{
|
||||||
const x = brickCenterX(index), y = brickCenterY(index);
|
const x = brickCenterX(index), y = brickCenterY(index);
|
||||||
if (!color) return;
|
if (!color) return;
|
||||||
const borderColor = ballsColor === color && puckColor || color !== "black" && redBorderOnBricksWithWrongColor && "red" || color;
|
const borderColor = ballsColor === color && puckColor || color !== "black" && redBorderOnBricksWithWrongColor && "red" || color;
|
||||||
drawBrick(ctx, color, borderColor, x, y);
|
drawBrick(canctx, color, borderColor, x, y);
|
||||||
if (color === "black") {
|
if (color === "black") {
|
||||||
ctx.globalCompositeOperation = "source-over";
|
canctx.globalCompositeOperation = "source-over";
|
||||||
drawIMG(ctx, bombSVG, brickWidth, x, y);
|
drawIMG(canctx, bombSVG, brickWidth, x, y);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
destinationCtx.drawImage(cachedBricksRender, offsetX, 0);
|
ctx.drawImage(cachedBricksRender, offsetX, 0);
|
||||||
}
|
}
|
||||||
let cachedGraphics = {};
|
let cachedGraphics = {};
|
||||||
function drawPuck(ctx, color, puckWidth, puckHeight, yoffset = 0) {
|
function drawPuck(ctx, color, puckWidth, puckHeight, yoffset = 0) {
|
||||||
|
@ -2260,9 +2294,9 @@ function drawBall(ctx, color, width, x, y, borderColor = "") {
|
||||||
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
|
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
|
||||||
}
|
}
|
||||||
const angles = 32;
|
const angles = 32;
|
||||||
function drawCoin(ctx, color, size, x, y, bg, rawAngle) {
|
function drawCoin(ctx, color, size, x, y, borderColor, rawAngle) {
|
||||||
const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles;
|
const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles;
|
||||||
const key = "coin with halo_" + color + "_" + size + "_" + bg + "_" + (color === "gold" ? angle : "whatever");
|
const key = "coin with halo_" + color + "_" + size + "_" + borderColor + "_" + (color === "gold" ? angle : "whatever");
|
||||||
if (!cachedGraphics[key]) {
|
if (!cachedGraphics[key]) {
|
||||||
const can = document.createElement("canvas");
|
const can = document.createElement("canvas");
|
||||||
can.width = size;
|
can.width = size;
|
||||||
|
@ -2274,7 +2308,7 @@ function drawCoin(ctx, color, size, x, y, bg, rawAngle) {
|
||||||
canctx.fillStyle = color;
|
canctx.fillStyle = color;
|
||||||
canctx.fill();
|
canctx.fill();
|
||||||
if (color === "gold") {
|
if (color === "gold") {
|
||||||
canctx.strokeStyle = bg;
|
canctx.strokeStyle = borderColor;
|
||||||
canctx.stroke();
|
canctx.stroke();
|
||||||
canctx.beginPath();
|
canctx.beginPath();
|
||||||
canctx.arc(size / 2, size / 2, size / 2 * 0.6, 0, 2 * Math.PI);
|
canctx.arc(size / 2, size / 2, size / 2 * 0.6, 0, 2 * Math.PI);
|
||||||
|
@ -2546,9 +2580,6 @@ function createExplosionSound(pan = 0.5) {
|
||||||
noiseSource.stop(context.currentTime + 1);
|
noiseSource.stop(context.currentTime + 1);
|
||||||
}
|
}
|
||||||
let levelTime = 0;
|
let levelTime = 0;
|
||||||
setInterval(()=>{
|
|
||||||
document.body.className = running ? " running " : " paused ";
|
|
||||||
}, 100);
|
|
||||||
window.addEventListener("visibilitychange", ()=>{
|
window.addEventListener("visibilitychange", ()=>{
|
||||||
if (document.hidden) pause(true);
|
if (document.hidden) pause(true);
|
||||||
});
|
});
|
||||||
|
@ -2631,7 +2662,7 @@ function isSettingOn(key) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
return cachedSettings[key] ?? options[key]?.default ?? false;
|
return cachedSettings[key] ?? (0, _options.options)[key]?.default ?? false;
|
||||||
}
|
}
|
||||||
function toggleSetting(key) {
|
function toggleSetting(key) {
|
||||||
cachedSettings[key] = !isSettingOn(key);
|
cachedSettings[key] = !isSettingOn(key);
|
||||||
|
@ -2641,93 +2672,50 @@ function toggleSetting(key) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
if (options[key].afterChange) options[key].afterChange();
|
if ((0, _options.options)[key].afterChange) (0, _options.options)[key].afterChange();
|
||||||
}
|
}
|
||||||
scoreDisplay.addEventListener("click", async (e)=>{
|
scoreDisplay.addEventListener("click", (e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openScorePanel();
|
openScorePanel().then();
|
||||||
});
|
});
|
||||||
async function openScorePanel() {
|
async function openScorePanel() {
|
||||||
pause(true);
|
pause(true);
|
||||||
const cb = await asyncAlert({
|
const cb = await asyncAlert({
|
||||||
title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`,
|
title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`,
|
||||||
text: `
|
text: `
|
||||||
<p>Upgrades picked so far : </p>
|
<p>Upgrades picked so far : </p>
|
||||||
<p>${pickedUpgradesHTMl()}</p>
|
<p>${pickedUpgradesHTMl()}</p>
|
||||||
`,
|
`,
|
||||||
allowClose: true,
|
allowClose: true,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
text: "Resume",
|
text: "Resume",
|
||||||
help: "Return to your run"
|
help: "Return to your run",
|
||||||
|
value: ()=>{}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Restart",
|
text: "Restart",
|
||||||
help: "Start a brand new run.",
|
help: "Start a brand new run.",
|
||||||
value: ()=>{
|
value: ()=>{
|
||||||
restart();
|
restart();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
if (cb) await cb();
|
if (cb) cb();
|
||||||
}
|
}
|
||||||
document.getElementById("menu").addEventListener("click", (e)=>{
|
document.getElementById("menu").addEventListener("click", (e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openSettingsPanel();
|
openSettingsPanel().then();
|
||||||
});
|
});
|
||||||
const options = {
|
|
||||||
sound: {
|
|
||||||
default: true,
|
|
||||||
name: `Game sounds`,
|
|
||||||
help: `Can slow down some phones.`,
|
|
||||||
disabled: ()=>false
|
|
||||||
},
|
|
||||||
"mobile-mode": {
|
|
||||||
default: window.innerHeight > window.innerWidth,
|
|
||||||
name: `Mobile mode`,
|
|
||||||
help: `Leaves space for your thumb.`,
|
|
||||||
afterChange () {
|
|
||||||
fitSize();
|
|
||||||
},
|
|
||||||
disabled: ()=>false
|
|
||||||
},
|
|
||||||
basic: {
|
|
||||||
default: false,
|
|
||||||
name: `Basic graphics`,
|
|
||||||
help: `Better performance on older devices.`,
|
|
||||||
disabled: ()=>false
|
|
||||||
},
|
|
||||||
pointerLock: {
|
|
||||||
default: false,
|
|
||||||
name: `Mouse pointer lock`,
|
|
||||||
help: `Locks and hides the mouse cursor.`,
|
|
||||||
disabled: ()=>!canvas.requestPointerLock
|
|
||||||
},
|
|
||||||
easy: {
|
|
||||||
default: false,
|
|
||||||
name: `Kids mode`,
|
|
||||||
help: `Start future runs with "slower ball".`,
|
|
||||||
disabled: ()=>false
|
|
||||||
},
|
|
||||||
record: {
|
|
||||||
default: false,
|
|
||||||
name: `Record gameplay videos`,
|
|
||||||
help: `Get a video of each level.`,
|
|
||||||
disabled () {
|
|
||||||
return window.location.search.includes("isInWebView=true");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
async function openSettingsPanel() {
|
async function openSettingsPanel() {
|
||||||
pause(true);
|
pause(true);
|
||||||
const optionsList = [];
|
const optionsList = [];
|
||||||
for(const key in options)if (options[key]) optionsList.push({
|
for(const key in 0, _options.options)if ((0, _options.options)[key]) optionsList.push({
|
||||||
disabled: options[key].disabled(),
|
disabled: (0, _options.options)[key].disabled(),
|
||||||
icon: isSettingOn(key) ? (0, _loadGameData.icons)["icon:checkmark_checked"] : (0, _loadGameData.icons)["icon:checkmark_unchecked"],
|
icon: isSettingOn(key) ? (0, _loadGameData.icons)["icon:checkmark_checked"] : (0, _loadGameData.icons)["icon:checkmark_unchecked"],
|
||||||
text: options[key].name,
|
text: (0, _options.options)[key].name,
|
||||||
help: options[key].help,
|
help: (0, _options.options)[key].help,
|
||||||
value: ()=>{
|
value: ()=>{
|
||||||
toggleSetting(key);
|
toggleSetting(key);
|
||||||
openSettingsPanel();
|
openSettingsPanel();
|
||||||
|
@ -2742,12 +2730,12 @@ async function openSettingsPanel() {
|
||||||
{
|
{
|
||||||
text: "Resume",
|
text: "Resume",
|
||||||
help: "Return to your run",
|
help: "Return to your run",
|
||||||
async value () {}
|
value () {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Starting perk",
|
text: "Starting perk",
|
||||||
help: "Try perks and levels you unlocked",
|
help: "Try perks and levels you unlocked",
|
||||||
async value () {
|
value () {
|
||||||
openUnlocksList();
|
openUnlocksList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2819,7 +2807,7 @@ async function openUnlocksList() {
|
||||||
},
|
},
|
||||||
icon
|
icon
|
||||||
})),
|
})),
|
||||||
...(0, _loadGameData.allLevels).sort((a, b)=>a.threshold - b.threshold).map((l, li)=>{
|
...(0, _loadGameData.allLevels).sort((a, b)=>a.threshold - b.threshold).map((l)=>{
|
||||||
const available = ts >= l.threshold;
|
const available = ts >= l.threshold;
|
||||||
return {
|
return {
|
||||||
text: l.name,
|
text: l.name,
|
||||||
|
@ -2956,18 +2944,18 @@ function attract(a, b, power) {
|
||||||
vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand
|
vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let mediaRecorder, captureStream, recordCanvas, recordCanvasCtx;
|
let mediaRecorder, captureStream, captureTrack, recordCanvas, recordCanvasCtx;
|
||||||
function recordOneFrame() {
|
function recordOneFrame() {
|
||||||
if (!isSettingOn("record")) return;
|
if (!isSettingOn("record")) return;
|
||||||
if (!running) return;
|
if (!running) return;
|
||||||
if (!captureStream) return;
|
if (!captureStream) return;
|
||||||
drawMainCanvasOnSmallCanvas();
|
drawMainCanvasOnSmallCanvas();
|
||||||
if (captureStream.requestFrame) captureStream.requestFrame();
|
if (captureTrack?.requestFrame) captureTrack?.requestFrame();
|
||||||
else captureStream.getVideoTracks()[0].requestFrame();
|
else if (captureStream?.requestFrame) captureStream.requestFrame();
|
||||||
}
|
}
|
||||||
function drawMainCanvasOnSmallCanvas() {
|
function drawMainCanvasOnSmallCanvas() {
|
||||||
if (!recordCanvasCtx) return;
|
if (!recordCanvasCtx) return;
|
||||||
recordCanvasCtx.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height);
|
recordCanvasCtx.drawImage(gameCanvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height);
|
||||||
// Here we don't use drawText as we don't want to cache a picture for each distinct value of score
|
// Here we don't use drawText as we don't want to cache a picture for each distinct value of score
|
||||||
recordCanvasCtx.fillStyle = "#FFF";
|
recordCanvasCtx.fillStyle = "#FFF";
|
||||||
recordCanvasCtx.textBaseline = "top";
|
recordCanvasCtx.textBaseline = "top";
|
||||||
|
@ -2980,13 +2968,14 @@ function drawMainCanvasOnSmallCanvas() {
|
||||||
function startRecordingGame() {
|
function startRecordingGame() {
|
||||||
if (!isSettingOn("record")) return;
|
if (!isSettingOn("record")) return;
|
||||||
if (!recordCanvas) {
|
if (!recordCanvas) {
|
||||||
// Smaller canvas with less details
|
// Smaller canvas with fewer details
|
||||||
recordCanvas = document.createElement("canvas");
|
recordCanvas = document.createElement("canvas");
|
||||||
recordCanvasCtx = recordCanvas.getContext("2d", {
|
recordCanvasCtx = recordCanvas.getContext("2d", {
|
||||||
antialias: false,
|
antialias: false,
|
||||||
alpha: false
|
alpha: false
|
||||||
});
|
});
|
||||||
captureStream = recordCanvas.captureStream(0);
|
captureStream = recordCanvas.captureStream(0);
|
||||||
|
captureTrack = captureStream.getVideoTracks()[0];
|
||||||
if (isSettingOn("sound") && getAudioContext() && audioRecordingTrack) captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0]);
|
if (isSettingOn("sound") && getAudioContext() && audioRecordingTrack) captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0]);
|
||||||
}
|
}
|
||||||
recordCanvas.width = gameZoneWidthRoundedUp;
|
recordCanvas.width = gameZoneWidthRoundedUp;
|
||||||
|
@ -3045,7 +3034,7 @@ function stopRecording() {
|
||||||
mediaRecorder?.stop();
|
mediaRecorder?.stop();
|
||||||
mediaRecorder = null;
|
mediaRecorder = null;
|
||||||
}
|
}
|
||||||
function captureFileName(ext) {
|
function captureFileName(ext = 'webm') {
|
||||||
return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, "-") + "." + ext;
|
return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, "-") + "." + ext;
|
||||||
}
|
}
|
||||||
function findLast(arr, predicate) {
|
function findLast(arr, predicate) {
|
||||||
|
@ -3055,11 +3044,11 @@ function findLast(arr, predicate) {
|
||||||
function toggleFullScreen() {
|
function toggleFullScreen() {
|
||||||
try {
|
try {
|
||||||
if (document.fullscreenElement !== null) {
|
if (document.fullscreenElement !== null) {
|
||||||
if (document.exitFullscreen) document.exitFullscreen();
|
if (document.exitFullscreen) document.exitFullscreen().then();
|
||||||
else if (document.webkitCancelFullScreen) document.webkitCancelFullScreen();
|
else if (document.webkitCancelFullScreen) document.webkitCancelFullScreen();
|
||||||
} else {
|
} else {
|
||||||
const docel = document.documentElement;
|
const docel = document.documentElement;
|
||||||
if (docel.requestFullscreen) docel.requestFullscreen();
|
if (docel.requestFullscreen) docel.requestFullscreen().then();
|
||||||
else if (docel.webkitRequestFullscreen) docel.webkitRequestFullscreen();
|
else if (docel.webkitRequestFullscreen) docel.webkitRequestFullscreen();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -3100,7 +3089,7 @@ fitSize();
|
||||||
restart();
|
restart();
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
},{"./loadGameData":"l1B4x"}],"l1B4x":[function(require,module,exports,__globalThis) {
|
},{"./loadGameData":"l1B4x","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l1B4x":[function(require,module,exports,__globalThis) {
|
||||||
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
||||||
parcelHelpers.defineInteropFlag(exports);
|
parcelHelpers.defineInteropFlag(exports);
|
||||||
parcelHelpers.export(exports, "appVersion", ()=>appVersion);
|
parcelHelpers.export(exports, "appVersion", ()=>appVersion);
|
||||||
|
@ -3389,8 +3378,8 @@ const rawUpgrades = [
|
||||||
id: "compound_interest",
|
id: "compound_interest",
|
||||||
giftable: true,
|
giftable: true,
|
||||||
name: "Compound interest",
|
name: "Compound interest",
|
||||||
max: 3,
|
max: 1,
|
||||||
help: (lvl)=>`+${lvl} combo / brick broken, -${lvl} combo per coin lost`,
|
help: ()=>`+1 combo per brick broken, resets on coin lost`,
|
||||||
fullHelp: `Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. Be sure however to catch every one of those coins
|
fullHelp: `Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. Be sure however to catch every one of those coins
|
||||||
with your puck, as any lost coin will decrease your combo by one point. One your combo is above the minimum, the bottom of the play area will
|
with your puck, as any lost coin will decrease your combo by one point. One your combo is above the minimum, the bottom of the play area will
|
||||||
have a red line to remind you that coins should not go there. This perk combines with other combo perks, the combo will rise faster but reset more easily.
|
have a red line to remind you that coins should not go there. This perk combines with other combo perks, the combo will rise faster but reset more easily.
|
||||||
|
@ -3585,7 +3574,56 @@ exports.export = function(dest, destName, get) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
},{}]},["hhTAC","3qndx"], "3qndx", "parcelRequire94c2")
|
},{}],"d5NoS":[function(require,module,exports,__globalThis) {
|
||||||
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
||||||
|
parcelHelpers.defineInteropFlag(exports);
|
||||||
|
parcelHelpers.export(exports, "options", ()=>options);
|
||||||
|
var _game = require("./game");
|
||||||
|
const options = {
|
||||||
|
sound: {
|
||||||
|
default: true,
|
||||||
|
name: `Game sounds`,
|
||||||
|
help: `Can slow down some phones.`,
|
||||||
|
disabled: ()=>false
|
||||||
|
},
|
||||||
|
"mobile-mode": {
|
||||||
|
default: window.innerHeight > window.innerWidth,
|
||||||
|
name: `Mobile mode`,
|
||||||
|
help: `Leaves space for your thumb.`,
|
||||||
|
afterChange () {
|
||||||
|
(0, _game.fitSize)();
|
||||||
|
},
|
||||||
|
disabled: ()=>false
|
||||||
|
},
|
||||||
|
basic: {
|
||||||
|
default: false,
|
||||||
|
name: `Basic graphics`,
|
||||||
|
help: `Better performance on older devices.`,
|
||||||
|
disabled: ()=>false
|
||||||
|
},
|
||||||
|
pointerLock: {
|
||||||
|
default: false,
|
||||||
|
name: `Mouse pointer lock`,
|
||||||
|
help: `Locks and hides the mouse cursor.`,
|
||||||
|
disabled: ()=>!(0, _game.gameCanvas).requestPointerLock
|
||||||
|
},
|
||||||
|
easy: {
|
||||||
|
default: false,
|
||||||
|
name: `Kids mode`,
|
||||||
|
help: `Start future runs with "slower ball".`,
|
||||||
|
disabled: ()=>false
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
default: false,
|
||||||
|
name: `Record gameplay videos`,
|
||||||
|
help: `Get a video of each level.`,
|
||||||
|
disabled () {
|
||||||
|
return window.location.search.includes("isInWebView=true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
},{"./game":"edeGs","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["hhTAC","3qndx"], "3qndx", "parcelRequire94c2")
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
414
src/game.ts
414
src/game.ts
File diff suppressed because it is too large
Load diff
54
src/options.ts
Normal file
54
src/options.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import {fitSize, gameCanvas} from "./game";
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
sound: {
|
||||||
|
default: true,
|
||||||
|
name: `Game sounds`,
|
||||||
|
help: `Can slow down some phones.`,
|
||||||
|
disabled: () => false,
|
||||||
|
},
|
||||||
|
"mobile-mode": {
|
||||||
|
default: window.innerHeight > window.innerWidth,
|
||||||
|
name: `Mobile mode`,
|
||||||
|
help: `Leaves space for your thumb.`,
|
||||||
|
afterChange() {
|
||||||
|
fitSize();
|
||||||
|
},
|
||||||
|
disabled: () => false,
|
||||||
|
},
|
||||||
|
basic: {
|
||||||
|
default: false,
|
||||||
|
name: `Basic graphics`,
|
||||||
|
help: `Better performance on older devices.`,
|
||||||
|
disabled: () => false,
|
||||||
|
},
|
||||||
|
pointerLock: {
|
||||||
|
default: false,
|
||||||
|
name: `Mouse pointer lock`,
|
||||||
|
help: `Locks and hides the mouse cursor.`,
|
||||||
|
disabled: () => !gameCanvas.requestPointerLock,
|
||||||
|
},
|
||||||
|
easy: {
|
||||||
|
default: false,
|
||||||
|
name: `Kids mode`,
|
||||||
|
help: `Start future runs with "slower ball".`,
|
||||||
|
disabled: () => false,
|
||||||
|
}, // Could not get the sharing to work without loading androidx and all the modern android things so for now i'll just disable sharing in the android app
|
||||||
|
record: {
|
||||||
|
default: false,
|
||||||
|
name: `Record gameplay videos`,
|
||||||
|
help: `Get a video of each level.`,
|
||||||
|
disabled() {
|
||||||
|
return window.location.search.includes("isInWebView=true");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as {[k:string]:OptionDef}
|
||||||
|
|
||||||
|
export type OptionDef = {
|
||||||
|
default:boolean;
|
||||||
|
name:string;
|
||||||
|
help:string;
|
||||||
|
disabled:()=>boolean
|
||||||
|
afterChange?:()=>void
|
||||||
|
}
|
||||||
|
export type OptionId = keyof (typeof options)
|
|
@ -212,8 +212,8 @@ export const rawUpgrades = [
|
||||||
id: "compound_interest",
|
id: "compound_interest",
|
||||||
giftable: true,
|
giftable: true,
|
||||||
name: "Compound interest",
|
name: "Compound interest",
|
||||||
max: 3,
|
max: 1,
|
||||||
help: (lvl) => `+${lvl} combo / brick broken, -${lvl} combo per coin lost`,
|
help: () => `+1 combo per brick broken, resets on coin lost`,
|
||||||
|
|
||||||
fullHelp: `Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. Be sure however to catch every one of those coins
|
fullHelp: `Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. Be sure however to catch every one of those coins
|
||||||
with your puck, as any lost coin will decrease your combo by one point. One your combo is above the minimum, the bottom of the play area will
|
with your puck, as any lost coin will decrease your combo by one point. One your combo is above the minimum, the bottom of the play area will
|
||||||
|
|
132
src/types.d.ts
vendored
132
src/types.d.ts
vendored
|
@ -1,4 +1,4 @@
|
||||||
import { rawUpgrades } from "./rawUpgrades";
|
import {rawUpgrades} from "./rawUpgrades";
|
||||||
|
|
||||||
export type colorString = string;
|
export type colorString = string;
|
||||||
|
|
||||||
|
@ -24,11 +24,11 @@ export type Palette = { [k: string]: string };
|
||||||
export type Upgrade = {
|
export type Upgrade = {
|
||||||
threshold: number;
|
threshold: number;
|
||||||
giftable: boolean;
|
giftable: boolean;
|
||||||
id: string;
|
id: PerkId;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
max: number;
|
max: number;
|
||||||
help: (lvl: string) => string;
|
help: (lvl: number) => string;
|
||||||
fullHelp: string;
|
fullHelp: string;
|
||||||
requires: PerkId | "";
|
requires: PerkId | "";
|
||||||
};
|
};
|
||||||
|
@ -39,65 +39,105 @@ declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
webkitAudioContext?: typeof AudioContext;
|
webkitAudioContext?: typeof AudioContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
webkitFullscreenEnabled?: boolean;
|
webkitFullscreenEnabled?: boolean;
|
||||||
webkitCancelFullScreen?: ()=>void;
|
webkitCancelFullScreen?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Element {
|
interface Element {
|
||||||
webkitRequestFullscreen: typeof Element.requestFullscreen
|
webkitRequestFullscreen: typeof Element.requestFullscreen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MediaStream {
|
||||||
|
// https://devdoc.net/web/developer.mozilla.org/en-US/docs/Web/API/CanvasCaptureMediaStream.html
|
||||||
|
// On firefox, the capture stream has the requestFrame option
|
||||||
|
// instead of the track, go figure
|
||||||
|
requestFrame?:()=>void
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Coin={
|
export type BallLike = {
|
||||||
points:number;
|
x: number;
|
||||||
|
y: number;
|
||||||
|
vx?: number;
|
||||||
|
vy?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Coin = {
|
||||||
|
points: number;
|
||||||
color: colorString;
|
color: colorString;
|
||||||
x:number;
|
x: number;
|
||||||
y:number;
|
y: number;
|
||||||
previousx:number;
|
previousx: number;
|
||||||
previousy:number;
|
previousy: number;
|
||||||
vx:number;
|
vx: number;
|
||||||
vy:number;
|
vy: number;
|
||||||
sx:number;
|
sx: number;
|
||||||
sy:number;
|
sy: number;
|
||||||
a:number;
|
a: number;
|
||||||
sa:number;
|
sa: number;
|
||||||
weight:number;
|
weight: number;
|
||||||
destroyed?:boolean;
|
destroyed?: boolean;
|
||||||
coloredABrick?:boolean;
|
coloredABrick?: boolean;
|
||||||
}
|
}
|
||||||
export type Ball = {
|
export type Ball = {
|
||||||
x:number;
|
x: number;
|
||||||
previousx:number;
|
previousx: number;
|
||||||
y:number;
|
y: number;
|
||||||
previousy:number;
|
previousy: number;
|
||||||
vx:number;
|
vx: number;
|
||||||
vy:number;
|
vy: number;
|
||||||
sx:number;
|
sx: number;
|
||||||
sy:number;
|
sy: number;
|
||||||
sparks:number;
|
sparks: number;
|
||||||
piercedSinceBounce:number;
|
piercedSinceBounce: number;
|
||||||
hitSinceBounce:number;
|
hitSinceBounce: number;
|
||||||
hitItem: {index:number, color:string}[],
|
hitItem: { index: number, color: string }[];
|
||||||
sapperUses:number;
|
bouncesList?: { x: number, y: number }[];
|
||||||
destroyed?:boolean;
|
sapperUses: number;
|
||||||
|
destroyed?: boolean;
|
||||||
|
previousvx?: number;
|
||||||
|
previousvy?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type FlashTypes= "text"|"particle"|'ball'
|
export type FlashTypes = "text" | "particle" | 'ball'
|
||||||
|
|
||||||
export type Flash = {
|
export type Flash = {
|
||||||
type: FlashTypes;
|
type: FlashTypes;
|
||||||
text?:string;
|
text?: string;
|
||||||
time:number;
|
time: number;
|
||||||
color:colorString;
|
color: colorString;
|
||||||
x:number;
|
x: number;
|
||||||
y:number;
|
y: number;
|
||||||
duration:number;
|
duration: number;
|
||||||
size:number;
|
size: number;
|
||||||
vx?:number;
|
vx?: number;
|
||||||
vy?:number;
|
vy?: number;
|
||||||
ethereal?:boolean;
|
ethereal?: boolean;
|
||||||
destroyed?:boolean;
|
destroyed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RunStats= {
|
||||||
|
started: number;
|
||||||
|
levelsPlayed: number;
|
||||||
|
runTime: number;
|
||||||
|
coins_spawned: number;
|
||||||
|
score: number;
|
||||||
|
bricks_broken: number;
|
||||||
|
misses: number;
|
||||||
|
balls_lost: number;
|
||||||
|
puck_bounces: number;
|
||||||
|
upgrades_picked: number;
|
||||||
|
max_combo: number;
|
||||||
|
max_level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type RunHistoryItem =RunStats & {
|
||||||
|
perks?: {[k in PerkId]:number};
|
||||||
|
appVersion?:string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue