mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-22 21:16:14 -04:00
5 colors /level, sound when ball or brick change color
This commit is contained in:
parent
b6fe46c9bc
commit
2e3ab3011f
21 changed files with 1379 additions and 598 deletions
144
src/game.ts
144
src/game.ts
|
@ -6,18 +6,19 @@ import {
|
|||
colorString,
|
||||
GameState,
|
||||
PerkId,
|
||||
PerksMap,
|
||||
RunHistoryItem, RunParams,
|
||||
RunHistoryItem,
|
||||
RunParams,
|
||||
RunStats,
|
||||
Upgrade,
|
||||
} from "./types";
|
||||
import {OptionId, options} from "./options";
|
||||
import {getAudioContext, getAudioRecordingTrack, sounds} from "./sounds";
|
||||
import {putBallsAtPuck, resetBalls} from "./resetBalls";
|
||||
import {sumOfKeys} from "./game_utils";
|
||||
import {makeEmptyPerksMap, sumOfKeys} from "./game_utils";
|
||||
import {baseCombo, decreaseCombo, resetCombo} from "./combo";
|
||||
|
||||
|
||||
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
|
||||
const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
|
||||
const ctx = gameCanvas.getContext("2d", {
|
||||
alpha: false,
|
||||
}) as CanvasRenderingContext2D;
|
||||
|
@ -29,69 +30,6 @@ bombSVG.src =
|
|||
<path d="m6.1528 26.516c-2.6992-3.4942-2.9332-8.281-.58305-11.981a10.454 10.454 0 017.3701-4.7582c1.962-.27726 4.1646.05953 5.8835.90027l.45013.22017.89782-.87417c.83748-.81464.91169-.87499 1.0992-.90271.40528-.058713.58876.03425 1.1971.6116l.55451.52679 1.0821-1.0821c1.1963-1.1963 1.383-1.3357 2.1039-1.5877.57898-.20223 1.5681-.19816 2.1691.00897 1.4613.50314 2.3673 1.7622 2.3567 3.2773-.0058.95654-.24464 1.5795-.90924 2.3746-.40936.48928-.55533.81057-.57898 1.2737-.02039.41018.1109.77714.42322 1.1792.30172.38816.3694.61323.2797.93044-.12803.45666-.56674.71598-1.0242.60507-.601-.14597-1.3031-1.3088-1.3969-2.3126-.09459-1.0161.19245-1.8682.92392-2.7432.42567-.50885.5643-.82851.5643-1.3031 0-.50151-.14026-.83177-.51211-1.2028-.50966-.50966-1.0968-.64829-1.781-.41996l-.37348.12477-2.1006 2.1006.52597.55696c.45421.48194.5325.58876.57898.78855.09622.41588.07502.45014-.88396 1.4548l-.87173.9125.26339.57979a10.193 10.193 0 01.9231 4.1001c.03996 2.046-.41996 3.8082-1.4442 5.537-.55044.928-1.0185 1.5013-1.8968 2.3241-.83503.78284-1.5526 1.2827-2.4904 1.7361-3.4266 1.657-7.4721 1.3422-10.549-.82035-.73473-.51782-1.7312-1.4621-2.2515-2.1357zm21.869-4.5584c-.0579-.19734-.05871-2.2662 0-2.4545.11906-.39142.57898-.63361 1.0038-.53005.23812.05708.54147.32455.6116.5382.06279.19163.06769 2.1805.0065 2.3811-.12558.40773-.61649.67602-1.0462.57164-.234-.05708-.51615-.30498-.57568-.50722m3.0417-2.6013c-.12313-.6222.37837-1.1049 1.0479-1.0079.18348.0261.25279.08399 1.0071.83911.75838.75838.81301.82362.84074 1.0112.10193.68499-.40365 1.1938-1.034 1.0405-.1949-.0473-.28786-.12558-1.0144-.85216-.7649-.76409-.80241-.81057-.84645-1.0316m.61323-3.0629a.85623.85623 0 01.59284-.99975c.28949-.09214 2.1814-.08318 2.3917.01141.38734.17369.6279.61078.53984.98181-.06035.25606-.35391.57327-.60181.64992-.25279.07747-2.2278.053-2.4097-.03017-.26013-.11906-.46318-.36125-.51374-.61323" fill="#fff" opacity="0.3"/>
|
||||
</svg>`);
|
||||
|
||||
const makeEmptyPerksMap = () => {
|
||||
const p = {} as any;
|
||||
upgrades.forEach((u) => (p[u.id] = 0));
|
||||
return p as PerksMap;
|
||||
};
|
||||
|
||||
|
||||
export function baseCombo() {
|
||||
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
|
||||
}
|
||||
|
||||
export function resetCombo(x: number | undefined, y: number | undefined) {
|
||||
const prev = gameState.combo;
|
||||
gameState.combo = baseCombo();
|
||||
if (!gameState.levelTime) {
|
||||
gameState.combo += gameState.perks.hot_start * 15;
|
||||
}
|
||||
if (prev > gameState.combo && gameState.perks.soft_reset) {
|
||||
gameState.combo += Math.floor((prev - gameState.combo) / (1 + gameState.perks.soft_reset));
|
||||
}
|
||||
const lost = Math.max(0, prev - gameState.combo);
|
||||
if (lost) {
|
||||
for (let i = 0; i < lost && i < 8; i++) {
|
||||
setTimeout(() => sounds.comboDecrease(), i * 100);
|
||||
}
|
||||
if (typeof x !== "undefined" && typeof y !== "undefined") {
|
||||
gameState.flashes.push({
|
||||
type: "text",
|
||||
text: "-" + lost,
|
||||
time: gameState.levelTime,
|
||||
color: "red",
|
||||
x: x,
|
||||
y: y,
|
||||
duration: 150,
|
||||
size: gameState.puckHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
return lost;
|
||||
}
|
||||
|
||||
export function decreaseCombo(by: number, x: number, y: number) {
|
||||
const prev = gameState.combo;
|
||||
gameState.combo = Math.max(baseCombo(), gameState.combo - by);
|
||||
const lost = Math.max(0, prev - gameState.combo);
|
||||
|
||||
if (lost) {
|
||||
sounds.comboDecrease();
|
||||
if (typeof x !== "undefined" && typeof y !== "undefined") {
|
||||
gameState.flashes.push({
|
||||
type: "text",
|
||||
text: "-" + lost,
|
||||
time: gameState.levelTime,
|
||||
color: "red",
|
||||
x: x,
|
||||
y: y,
|
||||
duration: 300,
|
||||
size: gameState.puckHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function play() {
|
||||
if (gameState.running) return;
|
||||
|
@ -341,7 +279,7 @@ async function openUpgradesPicker() {
|
|||
|
||||
gameState.runStatistics.upgrades_picked++;
|
||||
}
|
||||
resetCombo(undefined, undefined);
|
||||
resetCombo(gameState, undefined, undefined);
|
||||
resetBalls(gameState);
|
||||
}
|
||||
|
||||
|
@ -361,7 +299,7 @@ export function setLevel(l: number) {
|
|||
gameState.levelMisses = 0;
|
||||
gameState.runStatistics.levelsPlayed++;
|
||||
|
||||
resetCombo(undefined, undefined);
|
||||
resetCombo(gameState, undefined, undefined);
|
||||
recomputeTargetBaseSpeed();
|
||||
resetBalls(gameState);
|
||||
|
||||
|
@ -536,7 +474,7 @@ export function shouldPierceByColor(
|
|||
|
||||
export function coinBrickHitCheck(coin: Coin) {
|
||||
// Make ball/coin bonce, and return bricks that were hit
|
||||
const radius = gameState.coinSize / 2;
|
||||
const radius = coin.size / 2;
|
||||
const {x, y, previousX, previousY} = coin;
|
||||
|
||||
const vhit = hitsSomething(previousX, y, radius);
|
||||
|
@ -646,7 +584,7 @@ export function tick() {
|
|||
|
||||
if (gameState.levelTime > gameState.lastTickDown + 1000 && gameState.perks.hot_start) {
|
||||
gameState.lastTickDown = gameState.levelTime;
|
||||
decreaseCombo(gameState.perks.hot_start, gameState.puckPosition, gameState.gameZoneHeight - 2 * gameState.puckHeight);
|
||||
decreaseCombo(gameState, gameState.perks.hot_start, gameState.puckPosition, gameState.gameZoneHeight - 2 * gameState.puckHeight);
|
||||
}
|
||||
|
||||
if (remainingBricks <= gameState.perks.skip_last && !gameState.autoCleanUses) {
|
||||
|
@ -662,8 +600,8 @@ export function tick() {
|
|||
setLevel(gameState.currentLevel + 1);
|
||||
} else {
|
||||
gameOver(
|
||||
"Run finished with " + gameState.score + " points",
|
||||
"You cleared all levels for this run.",
|
||||
"Run finished ",
|
||||
`You cleared all levels for this run, catching ${gameState.score} coins in total.`,
|
||||
);
|
||||
}
|
||||
} else if (gameState.running || gameState.levelTime) {
|
||||
|
@ -698,7 +636,7 @@ export function tick() {
|
|||
coin.vy += delta * coin.weight * 0.8;
|
||||
|
||||
const speed = Math.abs(coin.sx) + Math.abs(coin.sx);
|
||||
const hitBorder = bordersHitCheck(coin, coinRadius, delta);
|
||||
const hitBorder = bordersHitCheck(coin, coin.size / 2, delta);
|
||||
|
||||
if (
|
||||
coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&
|
||||
|
@ -712,7 +650,7 @@ export function tick() {
|
|||
} else if (coin.y > gameState.canvasHeight + coinRadius) {
|
||||
coin.destroyed = true;
|
||||
if (gameState.perks.compound_interest) {
|
||||
resetCombo(coin.x, coin.y);
|
||||
resetCombo(gameState, coin.x, coin.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -727,6 +665,7 @@ export function tick() {
|
|||
) {
|
||||
gameState.bricks[hitBrick] = coin.color;
|
||||
coin.coloredABrick = true;
|
||||
sounds.colorChange(coin.x,0.3)
|
||||
}
|
||||
}
|
||||
if (typeof hitBrick !== "undefined" || hitBorder) {
|
||||
|
@ -783,10 +722,10 @@ export function tick() {
|
|||
});
|
||||
}
|
||||
|
||||
if (gameState.combo > baseCombo()) {
|
||||
if (gameState.combo > baseCombo(gameState)) {
|
||||
// The red should still be visible on a white bg
|
||||
const baseParticle = !isSettingOn("basic") &&
|
||||
(gameState.combo - baseCombo()) * Math.random() > 5 &&
|
||||
(gameState.combo - baseCombo(gameState)) * Math.random() > 5 &&
|
||||
gameState.running && {
|
||||
type: "particle" as const,
|
||||
duration: 100 * (Math.random() + 1),
|
||||
|
@ -955,7 +894,7 @@ export function ballTick(ball: Ball, delta: number) {
|
|||
borderHitCode % 2 &&
|
||||
ball.x < gameState.offsetX + gameState.gameZoneWidth / 2
|
||||
) {
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -963,11 +902,11 @@ export function ballTick(ball: Ball, delta: number) {
|
|||
borderHitCode % 2 &&
|
||||
ball.x > gameState.offsetX + gameState.gameZoneWidth / 2
|
||||
) {
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
}
|
||||
|
||||
if (gameState.perks.top_is_lava && borderHitCode >= 2) {
|
||||
resetCombo(ball.x, ball.y + gameState.ballSize);
|
||||
resetCombo(gameState, ball.x, ball.y + gameState.ballSize);
|
||||
}
|
||||
sounds.wallBeep(ball.x);
|
||||
ball.bouncesList?.push({x: ball.previousX, y: ball.previousY});
|
||||
|
@ -1010,7 +949,7 @@ export function ballTick(ball: Ball, delta: number) {
|
|||
}
|
||||
}
|
||||
if (gameState.perks.streak_shots) {
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
}
|
||||
|
||||
if (gameState.perks.respawn) {
|
||||
|
@ -1025,7 +964,7 @@ export function ballTick(ball: Ball, delta: number) {
|
|||
if (!ball.hitSinceBounce) {
|
||||
gameState.runStatistics.misses++;
|
||||
gameState.levelMisses++;
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
gameState.flashes.push({
|
||||
type: "text",
|
||||
text: "miss",
|
||||
|
@ -1245,6 +1184,11 @@ export function gameOver(title: string, intro: string) {
|
|||
});
|
||||
}
|
||||
|
||||
let unlockedItems = list.filter((u) => u.threshold > startTs && u.threshold < endTs);
|
||||
if (unlockedItems.length) {
|
||||
unlocksInfo += `<p>You unlocked ${unlockedItems.length} item(s) : ${unlockedItems.map(u => u.title).join(', ')}</p>`
|
||||
}
|
||||
|
||||
// Avoid the sad sound right as we restart a new games
|
||||
gameState.combo = 1;
|
||||
|
||||
|
@ -1254,6 +1198,7 @@ export function gameOver(title: string, intro: string) {
|
|||
text: `
|
||||
${gameState.isCreativeModeRun ? "<p>This test run and its score are not being recorded</p>" : ""}
|
||||
<p>${intro}</p>
|
||||
<p>Your total cumulative score went from ${startTs} to ${endTs}.</p>
|
||||
${unlocksInfo}
|
||||
`,
|
||||
actions: [
|
||||
|
@ -1480,6 +1425,7 @@ export function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
|
|||
|
||||
gameState.coins.push({
|
||||
points,
|
||||
size: gameState.coinSize,//-Math.floor(Math.log2(points)),
|
||||
color: gameState.perks.metamorphosis ? color : "gold",
|
||||
x: cx,
|
||||
y: cy,
|
||||
|
@ -1515,10 +1461,16 @@ export function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
|
|||
color
|
||||
) {
|
||||
if (gameState.perks.picky_eater) {
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
}
|
||||
|
||||
sounds.colorChange(ball.x,0.8)
|
||||
gameState.lastExplosion=gameState.levelTime
|
||||
gameState.ballsColor = color;
|
||||
if(!isSettingOn('basic')) {
|
||||
gameState.balls.forEach(ball=>{
|
||||
spawnExplosion(7, ball.previousX, ball.previousY, color, 150, 15)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
sounds.comboIncreaseMaybe(gameState.combo, ball.x, 1);
|
||||
}
|
||||
|
@ -1668,7 +1620,7 @@ export function render() {
|
|||
drawCoin(
|
||||
ctx,
|
||||
coin.color,
|
||||
gameState.coinSize,
|
||||
coin.size,
|
||||
coin.x,
|
||||
coin.y,
|
||||
level.color || "black",
|
||||
|
@ -1738,7 +1690,7 @@ export function render() {
|
|||
// The puck
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
if (gameState.perks.streak_shots && gameState.combo > baseCombo()) {
|
||||
if (gameState.perks.streak_shots && gameState.combo > baseCombo(gameState)) {
|
||||
drawPuck(ctx, "red", gameState.puckWidth, gameState.puckHeight, -2);
|
||||
}
|
||||
drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight);
|
||||
|
@ -1781,7 +1733,7 @@ export function render() {
|
|||
}
|
||||
}
|
||||
// Borders
|
||||
const hasCombo = gameState.combo > baseCombo();
|
||||
const hasCombo = gameState.combo > baseCombo(gameState);
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
if (gameState.offsetXRoundedDown) {
|
||||
// draw outside of gaming area to avoid capturing borders in recordings
|
||||
|
@ -1795,11 +1747,11 @@ export function render() {
|
|||
if (hasCombo && gameState.perks.right_is_lava) ctx.fillRect(width - 1, 0, 1, height);
|
||||
}
|
||||
|
||||
if (gameState.perks.top_is_lava && gameState.combo > baseCombo()) {
|
||||
if (gameState.perks.top_is_lava && gameState.combo > baseCombo(gameState)) {
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fillRect(gameState.offsetXRoundedDown, 0, gameState.gameZoneWidthRoundedUp, 1);
|
||||
}
|
||||
const redBottom = gameState.perks.compound_interest && gameState.combo > baseCombo();
|
||||
const redBottom = gameState.perks.compound_interest && gameState.combo > baseCombo(gameState);
|
||||
ctx.fillStyle = redBottom ? "red" : gameState.puckColor;
|
||||
if (isSettingOn("mobile-mode")) {
|
||||
ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight, gameState.gameZoneWidthRoundedUp, 1);
|
||||
|
@ -1837,7 +1789,7 @@ export function renderAllBricks() {
|
|||
ctx.globalAlpha = 1;
|
||||
|
||||
const redBorderOnBricksWithWrongColor =
|
||||
gameState.combo > baseCombo() && gameState.perks.picky_eater;
|
||||
gameState.combo > baseCombo(gameState) && gameState.perks.picky_eater;
|
||||
|
||||
const newKey =
|
||||
gameState.gameZoneWidth +
|
||||
|
@ -2447,11 +2399,11 @@ async function openSettingsPanel() {
|
|||
}
|
||||
}
|
||||
actions.push({
|
||||
text: "Creative mode",
|
||||
text: "Sandbox mode",
|
||||
help:
|
||||
getTotalScore() < creativeModeThreshold
|
||||
? "Unlocks at total score $" + creativeModeThreshold
|
||||
: "Test runs with custom perks",
|
||||
: "Test any perk combination",
|
||||
disabled: getTotalScore() < creativeModeThreshold,
|
||||
async value() {
|
||||
let creativeModePerks: Partial<{ [id in PerkId]: number }> = {},
|
||||
|
@ -2549,7 +2501,7 @@ async function openUnlocksList() {
|
|||
help:
|
||||
ts >= threshold ? fullHelp : `Unlocks at total score ${threshold}.`,
|
||||
disabled: ts < threshold,
|
||||
value: {perk: id} as RunParams,
|
||||
value: {perks: {[id]: 1}} as RunParams,
|
||||
icon,
|
||||
})),
|
||||
...allLevels
|
||||
|
@ -2944,7 +2896,7 @@ document.addEventListener("keydown", (e) => {
|
|||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", (e) => {
|
||||
document.addEventListener("keyup", async (e) => {
|
||||
const focused = document.querySelector("button:focus");
|
||||
if (e.key in pressed) {
|
||||
setKeyPressed(e.key, 0);
|
||||
|
@ -2966,6 +2918,8 @@ document.addEventListener("keyup", (e) => {
|
|||
openSettingsPanel();
|
||||
} else if (e.key.toLowerCase() === "s" && !alertsOpen) {
|
||||
openScorePanel();
|
||||
} else if (e.key.toLowerCase() === "r" && !alertsOpen) {
|
||||
// TODO
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
@ -2987,7 +2941,7 @@ function newGameState(params: RunParams): GameState {
|
|||
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
|
||||
);
|
||||
|
||||
const perks = {...makeEmptyPerksMap(), ...(params?.perks || {})}
|
||||
const perks = {...makeEmptyPerksMap(upgrades), ...(params?.perks || {})}
|
||||
|
||||
const gameState: GameState = {
|
||||
runLevels,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue