Stats display

This commit is contained in:
Renan LE CARO 2025-03-29 20:45:54 +01:00
parent 23798c4e58
commit a328520191
8 changed files with 288 additions and 122 deletions

View file

@ -61,6 +61,17 @@ body {
color: gold;
transition: color 0.01s;
}
span{
color: #333;
&.great{
color: lightgreen;
}
&.good{
color: white;
}
}
}
#menu {
@ -352,3 +363,22 @@ h2.histogram-title strong {
mix-blend-mode: luminosity;
}
}
//#statsdisplay{
// z-index: 1;
// white-space: nowrap;
// line-height: 20px;
// pointer-events: none;
// user-select: none;
// color: white;
// position: fixed;
// padding: 0;
// bottom: -20px;
// right: 0;
// width: 20px;
// overflow: visible;
//
// transform-origin: top left;
// transform: rotate(-90deg);
//
//}

View file

@ -160,11 +160,14 @@ setInterval(() => {
fitSize();
}, 1000);
export async function openShortRunUpgradesPicker(gameState: GameState) {
export async function openUpgradesPicker(gameState: GameState) {
const catchRate =
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1);
let repeats = 1;
let timeGain = "",
@ -983,10 +986,16 @@ restart(
(window.location.search.includes("stressTest") && {
level: "Bird",
perks: {
pierce: 1,
sapper: 1,
implosions: 3,
streak_shots:1
shocks:10,
multiball:6,
telekinesis:2,
ghost_coins:1,
pierce:4,
clairvoyant:3,
bigger_explosions:2,
sapper:2,
unbounded:1
},
levelsPerLoop: 2,
}) ||

View file

@ -36,7 +36,7 @@ import {icons, upgrades} from "./loadGameData";
import {addToTotalScore, getCurrentMaxCoins, getCurrentMaxParticles,} from "./settings";
import {background} from "./render";
import {gameOver} from "./gameOver";
import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openShortRunUpgradesPicker, pause,} from "./game";
import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openUpgradesPicker, pause,} from "./game";
import {stopRecording} from "./recording";
import {isOptionOn} from "./options";
import {isPremium} from "./premium";
@ -124,13 +124,13 @@ export function normalizeGameState(gameState: GameState) {
(gameState.gameZoneWidth / 12) *
Math.min(12, 3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck));
const corner = gameState.levelTime ? gameState.perks.corner_shot : 0
const corner = gameState.levelTime ? gameState.perks.corner_shot : 0
let minX = gameState.offsetXRoundedDown + gameState.puckWidth / 2 - gameState.puckWidth * corner
let minX = gameState.offsetXRoundedDown + gameState.puckWidth / 2 - gameState.puckWidth * corner
let maxX =gameState.offsetXRoundedDown +
gameState.gameZoneWidthRoundedUp -
gameState.puckWidth / 2 + gameState.puckWidth * corner;
let maxX = gameState.offsetXRoundedDown +
gameState.gameZoneWidthRoundedUp -
gameState.puckWidth / 2 + gameState.puckWidth * corner;
gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX);
@ -179,7 +179,7 @@ export function resetCombo(
}
if (typeof x !== "undefined" && typeof y !== "undefined") {
makeText(gameState, x, y, "red", "-" + lost, 20, 500+clamp(lost, 0,500));
makeText(gameState, x, y, "red", "-" + lost, 20, 500 + clamp(lost, 0, 500));
}
}
return lost;
@ -198,7 +198,7 @@ export function decreaseCombo(
if (lost) {
schedulGameSound(gameState, "comboDecrease", x, 1);
if (typeof x !== "undefined" && typeof y !== "undefined") {
makeText(gameState, x, y, "red", "-" + lost, 20, 400+lost);
makeText(gameState, x, y, "red", "-" + lost, 20, 400 + lost);
}
}
}
@ -256,8 +256,11 @@ export function explosionAt(
x: number,
y: number,
ball: Ball,
extraSize: number = 0
) {
const size = 1 + gameState.perks.bigger_explosions + Math.max(1,gameState.perks.implosions) - 1;
const size = 1 + gameState.perks.bigger_explosions +
Math.max(0, gameState.perks.implosions - 1) + extraSize
;
schedulGameSound(gameState, "explode", ball.x, 1);
if (index !== -1) {
const col = index % gameState.gridSize;
@ -292,7 +295,7 @@ export function explosionAt(
if (gameState.perks.implosions) {
spawnImplosion(
gameState,
7 *size ,
7 * size,
x,
y,
"white",
@ -300,7 +303,7 @@ export function explosionAt(
} else {
spawnExplosion(
gameState,
7 *size,
7 * size,
x,
y,
"white",
@ -332,7 +335,7 @@ export function explodeBrick(
// resetCombo(gameState, x, y);
// }
setBrick(gameState, index, "");
explosionAt(gameState, index, x, y, ball);
explosionAt(gameState, index, x, y, ball, 0);
} else if (color) {
// Even if it bounces we don't want to count that as a miss
@ -616,7 +619,7 @@ export async function setLevel(gameState: GameState, l: number) {
pause(false);
stopRecording();
if (l > 0) {
await openShortRunUpgradesPicker(gameState);
await openUpgradesPicker(gameState);
}
gameState.currentLevel = l;
@ -629,6 +632,7 @@ export async function setLevel(gameState: GameState, l: number) {
gameState.lastTickDown = gameState.levelTime;
gameState.levelStartScore = gameState.score;
gameState.levelSpawnedCoins = 0;
gameState.levelLostCoins = 0;
gameState.levelMisses = 0;
gameState.runStatistics.levelsPlayed++;
@ -650,7 +654,7 @@ export async function setLevel(gameState: GameState, l: number) {
gameState.gridSize = lvl.size;
fitSize();
}
empty(gameState.coins);
gameState.levelLostCoins += empty(gameState.coins);
empty(gameState.particles);
empty(gameState.lights);
empty(gameState.texts);
@ -798,7 +802,16 @@ export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
typeof hhit == "undefined" &&
hitsSomething(x, y, radius)) ||
undefined;
if (!gameState.perks.ghost_coins) {
if (gameState.perks.ghost_coins) {
// slow down
if (typeof (vhit ?? hhit ?? chit) !== "undefined") {
coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins;
coin.vx *= 1 - 0.2 / gameState.perks.ghost_coins;
}
} else {
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
coin.y = coin.previousY;
coin.vy *= -1;
@ -988,7 +1001,7 @@ export function gameStateTick(
(gameState.perks.viscosity *
0.03 +
0.005) *
frames;
frames / (1 + gameState.perks.etherealcoins);
coin.vy *= ratio;
coin.vx *= ratio;
@ -1040,6 +1053,7 @@ export function gameStateTick(
destroy(gameState.coins, coinIndex);
} else if (coin.y > gameState.canvasHeight + coinRadius) {
gameState.levelLostCoins+=coin.points
destroy(gameState.coins, coinIndex);
if (gameState.perks.compound_interest) {
resetCombo(gameState, coin.x, coin.y);
@ -1052,6 +1066,7 @@ export function gameStateTick(
)
) {
// Out of bound on sides
gameState.levelLostCoins+=coin.points
destroy(gameState.coins, coinIndex);
}
@ -1126,7 +1141,7 @@ export function gameStateTick(
((Math.random() - 0.5) * limit) / 3;
let index = brickIndex(x, y);
explosionAt(gameState, index, x, y, a);
explosionAt(gameState, index, x, y, a, Math.max(0, gameState.perks.shocks - 1));
}
}),
);
@ -1263,7 +1278,7 @@ export function gameStateTick(
forEachLiveOne(gameState.respawns, (r, ri) => {
if (gameState.bricks[r.index]) {
destroy(gameState.respawns, ri)
} else if (gameState.levelTime>r.time) {
} else if (gameState.levelTime > r.time) {
setBrick(gameState, r.index, r.color)
destroy(gameState.respawns, ri)
} else if (!isOptionOn("basic")) {
@ -1435,7 +1450,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
const angle = Math.atan2(
-gameState.puckWidth / 2,
(ball.x - gameState.puckPosition) *
(gameState.perks.concave_puck ? -0.5 : 1),
(gameState.perks.concave_puck ? -1 / (1 + gameState.perks.concave_puck) : 1),
);
ball.vx = speed * Math.cos(angle);
ball.vy = speed * Math.sin(angle);
@ -1657,7 +1672,8 @@ function makeCoin(
color = "gold",
points = 1,
) {
let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01)
weight *= 5 / (5 + gameState.perks.etherealcoins)
append(gameState.coins, (p: Partial<Coin>) => {
p.x = x;
@ -1718,7 +1734,7 @@ function makeText(
p.y = y;
p.color = color;
p.size = size;
p.duration = clamp(duration,400,2000);
p.duration = clamp(duration, 400, 2000);
p.text = text;
});
}
@ -1776,9 +1792,16 @@ export function liveCount<T>(where: ReusableArray<T>) {
}
export function empty<T>(where: ReusableArray<T>) {
let destroyed=0
where.total = 0;
where.indexMin = 0;
where.list.forEach((i) => (i.destroyed = true));
where.list.forEach((i) => {
if(!i.destroyed) {
i.destroyed = true
destroyed++
}
});
return destroyed
}
export function forEachLiveOne<T>(

View file

@ -23,6 +23,7 @@
<button id="menu"><span id="menuLabel">menu</span></button>
<button id="score"></button>
<div id="FPSDisplay"></div>
<div id="statsdisplay"></div>
<canvas id="game"></canvas>
<div id="popup">
<button id="close-modale"></button>

View file

@ -24,6 +24,7 @@ bombSVG.src =
btoa(`<svg width="144" height="144" viewBox="0 0 38.101 38.099" xmlns="http://www.w3.org/2000/svg">
<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>`);
bombSVG.onload = () => gameState.needsRender = true
export const background = document.createElement("img");
export const backgroundCanvas = document.createElement("canvas");
@ -49,7 +50,25 @@ export function render(gameState: GameState) {
} else {
menuLabel.innerText = t("play.menu_label");
}
scoreDisplay.innerText = `$${gameState.score}`;
const catchRate = gameState.levelSpawnedCoins ?
(gameState.levelSpawnedCoins - gameState.levelLostCoins)/gameState.levelSpawnedCoins :1
scoreDisplay.innerHTML= (isOptionOn('show_stats') ? `
<span class="${(catchRate==1 && 'great') || (catchRate>0.9 && 'good')||''}">
${Math.floor(catchRate*100)}%
</span><span> / </span>
<span class="${(gameState.levelWallBounces==0 && 'great') || (gameState.levelWallBounces<5 && 'good')||''}">
${gameState.levelWallBounces} B
</span><span> / </span>
<span class="${(gameState.levelTime<30000 && 'great') || (gameState.levelTime<60000 && 'good')||''}">
${Math.ceil(gameState.levelTime/1000)}s
</span><span> / </span>
<span class="${(gameState.levelMisses==0 && 'great') || (gameState.levelMisses<=3 && 'good')||''}">
${gameState.levelMisses} M
</span><span> / </span>
`: '' )+ `$${gameState.score}`;
scoreDisplay.className =
gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : "";
@ -117,10 +136,30 @@ export function render(gameState: GameState) {
) as CanvasRenderingContext2D;
bgctx.fillStyle = level.color || "#000";
bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
const pattern = ctx.createPattern(background, "repeat");
if (pattern) {
bgctx.fillStyle = pattern;
bgctx.fillRect(0, 0, width, height);
if (gameState.perks.clairvoyant >= 3) {
const pageSource = document.body.innerHTML.replace(/\s+/gi, '')
const lineWidth = Math.ceil(gameState.canvasWidth / 15)
const lines = Math.ceil(gameState.canvasHeight / 20)
const chars = lineWidth * lines
let start = Math.ceil(Math.random() * (pageSource.length - chars))
for (let i = 0; i < lines; i++) {
bgctx.fillStyle = 'white'
bgctx.font = '20px Courier'
bgctx.fillText(pageSource.slice(
start + i * lineWidth,
start + (i + 1) * lineWidth),
0,
i * 20,
gameState.canvasWidth
)
}
} else {
const pattern = ctx.createPattern(background, "repeat");
if (pattern) {
bgctx.fillStyle = pattern;
bgctx.fillRect(0, 0, width, height);
}
}
}
@ -174,9 +213,8 @@ export function render(gameState: GameState) {
coin.x,
coin.y,
(hasCombo && gameState.perks.asceticism && "red") ||
(!coin.points && "red") ||
level.color ||
"black",
(coin.color==='gold' && 'gold')||
gameState.puckColor,
coin.a,
);
});
@ -205,7 +243,7 @@ export function render(gameState: GameState) {
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, gameState.perks.clairvoyant >= 2);
});
ctx.globalCompositeOperation = "screen";
@ -290,11 +328,11 @@ export function render(gameState: GameState) {
drawPuck(
ctx,
gameState.puckColor,
gameState.puckColor,
gameState.puckWidth,
gameState.puckHeight,
0,
!!gameState.perks.concave_puck,
gameState.perks.concave_puck,
gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1,
);
@ -410,7 +448,7 @@ export function render(gameState: GameState) {
);
}
ctx.globalAlpha= gameState.perks.unbounded >1 ? 0.1 : 1
ctx.globalAlpha = gameState.perks.unbounded > 1 ? 0.1 : 1
drawStraightLine(
ctx,
gameState,
@ -422,7 +460,7 @@ export function render(gameState: GameState) {
1,
);
ctx.globalAlpha=1
ctx.globalAlpha = 1
drawStraightLine(
ctx,
gameState,
@ -448,6 +486,7 @@ export function render(gameState: GameState) {
);
}
if (shaked) {
ctx.resetTransform();
}
@ -559,7 +598,7 @@ export function renderAllBricks() {
countBricksAbove(gameState, index) &&
!countBricksBelow(gameState, index);
let redBorder = (gameState.ballsColor !== color &&
let redBorder = (gameState.ballsColor !== color &&
color !== "black" &&
redBorderOnBricksWithWrongColor) ||
(hasCombo && gameState.perks.zen && color === "black") ||
@ -567,7 +606,8 @@ export function renderAllBricks() {
redColorOnAllBricks;
canctx.globalCompositeOperation = "source-over";
drawBrick(canctx, color, x, y, redBorder ? offset : -1);
drawBrick(canctx,
color, x, y, redBorder ? offset : -1, gameState.perks.clairvoyant >= 2);
if (gameState.brickHP[index] > 1 && gameState.perks.clairvoyant) {
canctx.globalCompositeOperation = "destination-out";
drawText(
@ -598,7 +638,7 @@ export function drawPuck(
puckWidth: number,
puckHeight: number,
yOffset = 0,
flipped: boolean,
concave_puck: number,
redBorderOffset: number,
) {
const key =
@ -609,7 +649,7 @@ export function drawPuck(
"_" +
puckHeight +
"_" +
flipped +
concave_puck +
"_" +
redBorderOffset;
@ -623,13 +663,13 @@ export function drawPuck(
canctx.beginPath();
canctx.moveTo(0, puckHeight * 2);
if (flipped) {
if (concave_puck) {
canctx.lineTo(0, puckHeight * 0.75);
canctx.bezierCurveTo(
puckWidth / 2,
puckHeight,
puckHeight * (2 + concave_puck) / 3,
puckWidth / 2,
puckHeight * 1,
puckHeight * (2 + concave_puck) / 3,
puckWidth,
puckHeight * 0.75,
);
@ -741,14 +781,12 @@ export function drawCoin(
canctx.fillStyle = color;
canctx.fill();
if (color === "gold" || borderColor === "red") {
canctx.strokeStyle = borderColor;
if (borderColor == "red") {
canctx.lineWidth = 2;
canctx.setLineDash(redBorderDash);
}
canctx.stroke();
canctx.strokeStyle = borderColor;
if (borderColor == "red") {
canctx.lineWidth = 2;
canctx.setLineDash(redBorderDash);
}
canctx.stroke();
if (color === "gold") {
// Fill in
@ -817,6 +855,7 @@ export function drawBrick(
x: number,
y: number,
offset: number = 0,
borderOnly: boolean
) {
const tlx = Math.ceil(x - gameState.brickWidth / 2);
const tly = Math.ceil(y - gameState.brickWidth / 2);
@ -825,7 +864,7 @@ export function drawBrick(
const width = brx - tlx,
height = bry - tly;
const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset;
const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + '_' + borderOnly;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
@ -850,7 +889,9 @@ export function drawBrick(
height - bord,
cornerRadius,
);
canctx.fill();
if (!borderOnly) {
canctx.fill();
}
canctx.stroke();
cachedGraphics[key] = can;

1
src/types.d.ts vendored
View file

@ -244,6 +244,7 @@ export type GameState = {
levelStartScore: number;
levelMisses: number;
levelSpawnedCoins: number;
levelLostCoins: number;
// MAX_COINS: number;
// MAX_PARTICLES: number;