Fixed an issue with resizing and pacman level, changed ball attracts coins

This commit is contained in:
Renan LE CARO 2025-04-09 09:24:15 +02:00
parent 83b9c0dec5
commit 183a11f989
10 changed files with 1837 additions and 1761 deletions

View file

@ -28,11 +28,11 @@ Some upgrades currently are not really useful
## To do ## To do
- change fortunate ball to work more like coin magnet, carrying the balls around to catch them at next puck bounce
- add a test to forbid more than 5% grey bricks on black background, remove grey bricks border
## Done ## Done
- change fortunate ball to work more like coin magnet, carrying the balls around to catch them at next puck bounce
- add a test to forbid more than 5% grey bricks on black background, remove grey bricks border
- simplified texts to make translation easier - simplified texts to make translation easier
- fixed some issues around saved level unlocks - fixed some issues around saved level unlocks
- change donation text to not suggest an amount - change donation text to not suggest an amount

View file

@ -1,4 +1,4 @@
// npx nodemon checks.js // npx nodemon checks.js
const fs= require('fs') const fs= require('fs')
const english = JSON.parse(fs.readFileSync('./src/i18n/en.json')) const english = JSON.parse(fs.readFileSync('./src/i18n/en.json'))
console.log(Object.entries(english).sort((a,b)=>a[1].length-b[1].length).slice(-10,-1).map(([k,v])=>k+'\n'+k.split('').map(c=>'=').join('')+'\n\n'+v).join('\n\n')) console.debug(Object.entries(english).sort((a,b)=>a[1].length-b[1].length).slice(-10,-1).map(([k,v])=>k+'\n'+k.split('').map(c=>'=').join('')+'\n\n'+v).join('\n\n'))

56
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -10,13 +10,11 @@ app.use(bodyParser.text({
})); }));
app.get('/src/data/levels.json', (req, res) => { app.get('/src/data/levels.json', (req, res) => {
console.log('src/data/levels.json')
res.json(JSON.parse(fs.readFileSync('src/data/levels.json'))) res.json(JSON.parse(fs.readFileSync('src/data/levels.json')))
}) })
app.post('/src/data/levels.json', (req, res) => { app.post('/src/data/levels.json', (req, res) => {
if(req.body?.trim()) { if(req.body?.trim()) {
console.log('Levels updated')
fs.writeFileSync('src/data/levels.json', req.body) fs.writeFileSync('src/data/levels.json', req.body)
} }
res.end('OK') res.end('OK')

View file

@ -77,7 +77,7 @@
"size": 6, "size": 6,
"bricks": "_______gggg__rrrr__yyyy", "bricks": "_______gggg__rrrr__yyyy",
"svg": 8, "svg": 8,
"color": "" "color": "#5da3ea"
}, },
{ {
"name": "France", "name": "France",
@ -413,12 +413,13 @@
"name": "Wiki", "name": "Wiki",
"size": 10, "size": 10,
"bricks": "_______________________GGGG_____GGkkGG___GkggggkG__GgWWWWgG__GkggggkG___GGkkGG_____GGGG_______________________", "bricks": "_______________________GGGG_____GGkkGG___GkggggkG__GgWWWWgG__GkggggkG___GGkkGG_____GGGG_______________________",
"svg": null "svg": null,
"color": "#1c71d8"
}, },
{ {
"name": "Baby Dog", "name": "Baby Dog",
"size": 8, "size": 8,
"bricks": "_______W__eeeeWWWWeeWeWWWegWegeeeeWWWWee_eWggWe__eWWWWe____WW", "bricks": "_______W__eeeeWWWWeeWeWWWeBWeBeeeeWWWWee_eWBBWe__eWWWWe____WW___",
"svg": null "svg": null
}, },
{ {
@ -570,7 +571,7 @@
{ {
"name": "icon:soft_reset", "name": "icon:soft_reset",
"size": 8, "size": 8,
"bricks": "___rg_____rrgg___rryggg_rryWggggrryWgggg_ryyggg___rrgg_____rg___", "bricks": "____yy______yyy_____yyyy____yyyyyyyyyyyyyyyyyyyy_yyyyyy___yyyy__",
"svg": null "svg": null
}, },
{ {
@ -782,7 +783,7 @@
"size": 8, "size": 8,
"bricks": "_________tttttt__tttttt__gggggg__gggggg__WWWWWW__WWWWWW", "bricks": "_________tttttt__tttttt__gggggg__gggggg__WWWWWW__WWWWWW",
"svg": null, "svg": null,
"color": "#986a44" "color": "#26a269"
}, },
{ {
"name": "Finland", "name": "Finland",
@ -1039,7 +1040,7 @@
"size": 24, "size": 24,
"bricks": "____________________________________________________ggggg______ggggg_______gg___g______g___gg_____gg________________gg___gg__________________gg_gggggggggg____gggggggggggggtttttggggggggbbbbbgggggtWWWttttggggbbbbWWWbgg_gtWttttttggggbbbbWbbbg__gtttttttgg__ggbbbbbbbg__gtttttttg____gbbbbbbbg__ggtttttgg____ggbbbbbgg___ggtttgg______ggbbbgg_____ggggg________ggggg___________________________________________________________________________________________________________________________________________________________________________________________________________________________", "bricks": "____________________________________________________ggggg______ggggg_______gg___g______g___gg_____gg________________gg___gg__________________gg_gggggggggg____gggggggggggggtttttggggggggbbbbbgggggtWWWttttggggbbbbWWWbgg_gtWttttttggggbbbbWbbbg__gtttttttgg__ggbbbbbbbg__gtttttttg____gbbbbbbbg__ggtttttgg____ggbbbbbgg___ggtttgg______ggbbbgg_____ggggg________ggggg___________________________________________________________________________________________________________________________________________________________________________________________________________________________",
"svg": null, "svg": null,
"color": "#1a5fb4", "color": "#26a269",
"credit": "https://prohama.com/sunglasses-pattern-1/" "credit": "https://prohama.com/sunglasses-pattern-1/"
}, },
{ {
@ -1109,7 +1110,7 @@
{ {
"name": "icon:fountain_toss", "name": "icon:fountain_toss",
"size": 12, "size": 12,
"bricks": "________________tttt______tttggttt____tggggggt____t__gg__t____tllggllt___ltbyggbbtl_lbtttggtytblgyttybbtttyggggttbbtyggg_gggggggggg____gggggg___", "bricks": "__________________________________________________WWWWWWWW___WttttttttW_WtytttytyttWWtttyttttttWlWtyttttytWl_lWWWWWWWWl___llllllll______________",
"svg": null, "svg": null,
"color": "" "color": ""
}, },
@ -1122,8 +1123,8 @@
}, },
{ {
"name": "Gear", "name": "Gear",
"size": 14, "size": 13,
"bricks": "_________________________________l_l_l_______l_lllll_l______lllllll_____lllll_lllll____lll___lll____lll_____lll____lll___lll____lllll_lllll_____lllllll______l_lllll_l_______l_l_l__________________", "bricks": "_________________l_l_l______l_lllll_l_____lllllll____lllll_lllll___lll___lll___lll_____lll___lll___lll___lllll_lllll____lllllll_____l_lllll_l______l_l_l_________________",
"svg": null, "svg": null,
"color": "" "color": ""
}, },
@ -1279,7 +1280,7 @@
"size": 11, "size": 11,
"bricks": "___gggg_____gggrrgg_____ggrrg_______gggg_____gggyygg_____ggyyg_______gggg_____gggCCgg_____ggCCg_______gggg________gg_____", "bricks": "___gggg_____gggrrgg_____ggrrg_______gggg_____gggyygg_____ggyyg_______gggg_____gggCCgg_____ggCCg_______gggg________gg_____",
"svg": null, "svg": null,
"color": "", "color": "#240a8b",
"credit": "Left a wonderful review on the play store." "credit": "Left a wonderful review on the play store."
}, },
{ {
@ -1287,7 +1288,7 @@
"size": 13, "size": 13,
"bricks": "_______________________________________OOOORgRgRgOOOOWOORgRgRgOOOOOWORgRgRgOWOOWOORgRgRgOOWOOWORgRgRgOWOOWOORgRgRgOOOOOOORgRgRgOOO_______________________________________", "bricks": "_______________________________________OOOORgRgRgOOOOWOORgRgRgOOOOOWORgRgRgOWOOWOORgRgRgOOWOOWORgRgRgOWOOWOORgRgRgOOOOOOORgRgRgOOO_______________________________________",
"svg": null, "svg": null,
"color": "", "color": "#62a0ea",
"credit": "Colin helped a lot with the game design https://colin-crapahute.bearblog.dev/" "credit": "Colin helped a lot with the game design https://colin-crapahute.bearblog.dev/"
}, },
{ {
@ -1295,7 +1296,7 @@
"size": 15, "size": 15,
"bricks": "_________________________________ggggggggg_____g_________g___g___________g_g_____________gg_____________gg_____yyy_____ggg__yyyyyyy__ggggtyyyyyyyyytggggtttttttttttgggg_ttttttttt_gg_____ttttt___________________________________", "bricks": "_________________________________ggggggggg_____g_________g___g___________g_g_____________gg_____________gg_____yyy_____ggg__yyyyyyy__ggggtyyyyyyyyytggggtttttttttttgggg_ttttttttt_gg_____ttttt___________________________________",
"svg": null, "svg": null,
"color": "", "color": "#240a8b",
"credit": "Early adopter of the game" "credit": "Early adopter of the game"
}, },
{ {
@ -1309,7 +1310,7 @@
{ {
"name": "icon:minefield", "name": "icon:minefield",
"size": 7, "size": 7,
"bricks": "W__W__W_W___W___W_W__W_____W_W___W____g_____ggg__", "bricks": "W__W__W_W___W___W_W__W_____W_W___W____l_____lll__",
"svg": null, "svg": null,
"color": "" "color": ""
}, },

View file

@ -3,7 +3,7 @@
"B": "black", "B": "black",
"W": "#FFFFFF", "W": "#FFFFFF",
"g": "#231f20", "g": "#231f20",
"y": "#ffd300", "y": "#FFD300",
"b": "#6262EA", "b": "#6262EA",
"t": "#5DA3EA", "t": "#5DA3EA",
"s": "#E67070", "s": "#E67070",

View file

@ -156,6 +156,7 @@ export const fitSize = () => {
Math.min(gameState.canvasWidth, gameState.gameZoneHeight * 0.73), Math.min(gameState.canvasWidth, gameState.gameZoneHeight * 0.73),
); );
gameState.brickWidth = Math.floor(baseWidth / gameState.gridSize / 2) * 2; gameState.brickWidth = Math.floor(baseWidth / gameState.gridSize / 2) * 2;
gameState.gameZoneWidth = gameState.brickWidth * gameState.gridSize; gameState.gameZoneWidth = gameState.brickWidth * gameState.gridSize;
gameState.offsetX = Math.floor( gameState.offsetX = Math.floor(
(gameState.canvasWidth - gameState.gameZoneWidth) / 2, (gameState.canvasWidth - gameState.gameZoneWidth) / 2,
@ -974,8 +975,10 @@ document.addEventListener("keyup", async (e) => {
export const gameState = newGameState({}); export const gameState = newGameState({});
export function restart(params: RunParams) { export function restart(params: RunParams) {
fitSize(); // fitSize();
Object.assign(gameState, newGameState(params)); Object.assign(gameState, newGameState(params));
// Recompute brick size according to level
fitSize();
pauseRecording(); pauseRecording();
setLevel(gameState, 0); setLevel(gameState, 0);
} }

View file

@ -28,25 +28,17 @@ import {
telekinesisEffectRate, telekinesisEffectRate,
yoyoEffectRate, yoyoEffectRate,
} from "./game_utils"; } from "./game_utils";
import { t } from "./i18n/i18n"; import {t} from "./i18n/i18n";
import { icons } from "./loadGameData"; import {icons} from "./loadGameData";
import { getCurrentMaxCoins, getCurrentMaxParticles } from "./settings"; import {getCurrentMaxCoins, getCurrentMaxParticles} from "./settings";
import { background } from "./render"; import {background} from "./render";
import { gameOver } from "./gameOver"; import {gameOver} from "./gameOver";
import { import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openUpgradesPicker, pause,} from "./game";
brickIndex, import {stopRecording} from "./recording";
fitSize, import {isOptionOn} from "./options";
gameState, import {clamp, comboKeepingRate} from "./pure_functions";
hasBrick, import {addToTotalScore} from "./addToTotalScore";
hitsSomething,
openUpgradesPicker,
pause,
} from "./game";
import { stopRecording } from "./recording";
import { isOptionOn } from "./options";
import { clamp, comboKeepingRate } from "./pure_functions";
import { addToTotalScore } from "./addToTotalScore";
export function setMousePos(gameState: GameState, x: number) { export function setMousePos(gameState: GameState, x: number) {
gameState.puckPosition = x; gameState.puckPosition = x;
@ -414,7 +406,7 @@ export function explodeBrick(
while (coinsToSpawn > 0) { while (coinsToSpawn > 0) {
const points = Math.min(pointsPerCoin, coinsToSpawn); const points = Math.min(pointsPerCoin, coinsToSpawn);
if (points < 0 || isNaN(points)) { if (points < 0 || isNaN(points)) {
console.error({ points }); console.error({points});
debugger; debugger;
} }
@ -834,7 +826,7 @@ export function attract(gameState: GameState, a: Ball, b: Ball, power: number) {
export function coinBrickHitCheck(gameState: GameState, coin: Coin) { export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
// Make ball/coin bonce, and return bricks that were hit // Make ball/coin bonce, and return bricks that were hit
const radius = coin.size / 2; const radius = coin.size / 2;
const { x, y, previousX, previousY } = coin; const {x, y, previousX, previousY} = coin;
const vhit = hitsSomething(previousX, y, radius); const vhit = hitsSomething(previousX, y, radius);
const hhit = hitsSomething(x, previousY, radius); const hhit = hitsSomething(x, previousY, radius);
@ -1017,7 +1009,7 @@ export function gameStateTick(
} else { } else {
gameOver( gameOver(
t("gameOver.win.title"), t("gameOver.win.title"),
t("gameOver.win.summary", { score: gameState.score }), t("gameOver.win.summary", {score: gameState.score}),
); );
} }
} else if (gameState.running || gameState.levelTime) { } else if (gameState.running || gameState.levelTime) {
@ -1042,13 +1034,45 @@ export function gameStateTick(
} }
if (gameState.perks.ball_attracts_coins) { if (gameState.perks.ball_attracts_coins) {
// Find closest ball
let closestBall = gameState.balls[0]
let dist = distance2(closestBall, coin)
gameState.balls.forEach((ball) => { gameState.balls.forEach((ball) => {
if (ball == closestBall) return
const d2 = distance2(ball, coin); const d2 = distance2(ball, coin);
coin.vx += if (d2 < dist) {
((ball.x - coin.x) / d2) * 50 * gameState.perks.ball_attracts_coins; closestBall = ball
coin.vy += dist = d2
((ball.y - coin.y) / d2) * 50 * gameState.perks.ball_attracts_coins; }
}); });
const minDist = gameState.brickWidth * gameState.brickWidth
if (dist > minDist && dist < minDist * 4 * 4*gameState.perks.ball_attracts_coins) {
// Slow down coins in effect radius
const ratio = 1 - 0.02 * (0.5 + gameState.perks.ball_attracts_coins);
coin.vx *= ratio;
coin.vy *= ratio;
coin.vy *= ratio;
// Carry them
const dx = ((closestBall.x - coin.x) / dist) * 50 * gameState.perks.ball_attracts_coins
const dy = ((closestBall.y - coin.y) / dist) * 50 * gameState.perks.ball_attracts_coins;
coin.vx += dx;
coin.vy += dy
if (!isOptionOn('basic') && Math.random()*gameState.perks.ball_attracts_coins > 0.9 ) {
makeParticle(
gameState,
coin.x+dx*5,
coin.y+dy*5,
dx*2,
dy*2,
rainbowColor(),
true,
gameState.coinSize / 2,
100,
);
}
}
} }
const ratio = const ratio =
@ -1346,7 +1370,7 @@ export function gameStateTick(
setBrick(gameState, r.index, r.color); setBrick(gameState, r.index, r.color);
destroy(gameState.respawns, ri); destroy(gameState.respawns, ri);
} else { } else {
const { index, color } = r; const {index, color} = r;
const vertical = Math.random() > 0.5; const vertical = Math.random() > 0.5;
const dx = Math.random() > 0.5 ? 1 : -1; const dx = Math.random() > 0.5 ? 1 : -1;
const dy = Math.random() > 0.5 ? 1 : -1; const dy = Math.random() > 0.5 ? 1 : -1;
@ -1587,13 +1611,13 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
if (!gameState.balls.find((b) => !b.destroyed)) { if (!gameState.balls.find((b) => !b.destroyed)) {
gameOver( gameOver(
t("gameOver.lost.title"), t("gameOver.lost.title"),
t("gameOver.lost.summary", { score: gameState.score }), t("gameOver.lost.summary", {score: gameState.score}),
); );
} }
} }
const radius = gameState.ballSize / 2; const radius = gameState.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 } = ball; 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);
@ -1851,7 +1875,7 @@ export function append<T>(
makeItem(where.list[where.indexMin]); makeItem(where.list[where.indexMin]);
where.indexMin++; where.indexMin++;
} else { } else {
const p = { destroyed: false }; const p = {destroyed: false};
makeItem(p); makeItem(p);
where.list.push(p); where.list.push(p);
} }

View file

@ -1,6 +1,7 @@
import _palette from "./data/palette.json"; import _palette from "./data/palette.json";
import _rawLevelsList from "./data/levels.json"; import _rawLevelsList from "./data/levels.json";
import _appVersion from "./data/version.json"; import _appVersion from "./data/version.json";
import {rawUpgrades} from "./upgrades";
describe("json data checks", () => { describe("json data checks", () => {
it("_rawLevelsList has icon levels", () => { it("_rawLevelsList has icon levels", () => {
@ -8,6 +9,14 @@ describe("json data checks", () => {
_rawLevelsList.filter((l) => l.name.startsWith("icon:")).length, _rawLevelsList.filter((l) => l.name.startsWith("icon:")).length,
).toBeGreaterThan(10); ).toBeGreaterThan(10);
}); });
it("all upgrades have icons", () => {
const missingIcon = rawUpgrades.filter((u) => !_rawLevelsList.find(l=>l.name=='icon:'+u.id))
expect(
missingIcon,
).toEqual([]);
});
it("_rawLevelsList has non-icon few levels", () => { it("_rawLevelsList has non-icon few levels", () => {
expect( expect(
_rawLevelsList.filter((l) => !l.name.startsWith("icon:")).length, _rawLevelsList.filter((l) => !l.name.startsWith("icon:")).length,
@ -29,6 +38,18 @@ describe("json data checks", () => {
it("Has a few colors", () => { it("Has a few colors", () => {
expect(Object.keys(_palette).length).toBeGreaterThan(10); expect(Object.keys(_palette).length).toBeGreaterThan(10);
}); });
it("Avoids dark bricks on dark bg", () => {
const levelsWithDarkBricksAndBG = _rawLevelsList
.filter(l=>!l.color && !l.name.match(/^icon:/))
.map(l=>({
name:l.name,
bricks:l.bricks.split('').filter(c=>c!=='_').length,
darkBricks:l.bricks.split('').filter(c=>c==='g').length,
}))
.filter(l=>l.darkBricks>0.05*l.bricks)
expect(levelsWithDarkBricksAndBG).toEqual([]);
});
it("Has an _appVersion", () => { it("Has an _appVersion", () => {
expect(parseInt(_appVersion)).toBeGreaterThan(2000); expect(parseInt(_appVersion)).toBeGreaterThan(2000);
}); });

View file

@ -257,11 +257,10 @@ export function render(gameState: GameState) {
coin.size, coin.size,
coin.x, coin.x,
coin.y, coin.y,
// Red border around coins with asceticism
(hasCombo && gameState.perks.asceticism && "#FF0000") || (hasCombo && gameState.perks.asceticism && "#FF0000") ||
(color === "#ffd300" && "#ffd300") || // Gold coins
(color == "#231f20" && // (color === "#ffd300" && "#ffd300") ||
gameState.level.color == "#000000" &&
"#FFFFFF") ||
gameState.level.color, gameState.level.color,
coin.a, coin.a,
); );
@ -408,7 +407,7 @@ export function render(gameState: GameState) {
gameState.coinSize, gameState.coinSize,
left + gameState.coinSize / 2, left + gameState.coinSize / 2,
gameState.gameZoneHeight - gameState.puckHeight / 2, gameState.gameZoneHeight - gameState.puckHeight / 2,
gameState.puckColor, "#ffd300",
0, 0,
); );
drawText( drawText(
@ -895,9 +894,15 @@ export function drawFuzzyBall(
x: number, x: number,
y: number, y: number,
) { ) {
const key = "fuzzy-circle" + color + "_" + width; const key = "fuzzy-circle" + color + "_" + width;
if (!color) debugger; if (!color?.startsWith('#')) debugger;
const size = Math.round(width * 3); const size = Math.round(width * 3);
if (!size || isNaN(size)) {
debugger;
return
}
if (!cachedGraphics[key]) { if (!cachedGraphics[key]) {
const can = document.createElement("canvas"); const can = document.createElement("canvas");
can.width = size; can.width = size;
@ -919,6 +924,7 @@ export function drawFuzzyBall(
canctx.fillStyle = gradient; canctx.fillStyle = gradient;
canctx.fillRect(0, 0, size, size); canctx.fillRect(0, 0, size, size);
cachedGraphics[key] = can; cachedGraphics[key] = can;
} }
ctx.drawImage( ctx.drawImage(
cachedGraphics[key], cachedGraphics[key],
@ -944,12 +950,6 @@ export function drawBrick(
const width = brx - tlx, const width = brx - tlx,
height = bry - tly; height = bry - tly;
const whiteBorder =
offset == -1 &&
color == "#231f20" &&
gameState.level.color == "#000000" &&
"#FFFFFF";
const key = const key =
"brick" + "brick" +
color + color +
@ -962,8 +962,7 @@ export function drawBrick(
offset + offset +
"_" + "_" +
borderOnly + borderOnly +
"_" + "_" ;
whiteBorder;
if (!cachedGraphics[key]) { if (!cachedGraphics[key]) {
const can = document.createElement("canvas"); const can = document.createElement("canvas");
@ -977,9 +976,9 @@ export function drawBrick(
canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray); canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray);
canctx.lineDashOffset = offset; canctx.lineDashOffset = offset;
canctx.strokeStyle = (offset !== -1 && "#FF000033") || whiteBorder || color; canctx.strokeStyle = (offset !== -1 && "#FF000033") || color;
canctx.lineJoin = "round"; canctx.lineJoin = "round";
canctx.lineWidth = whiteBorder ? 1 : bord; canctx.lineWidth = bord;
roundRect( roundRect(
canctx, canctx,
bord / 2, bord / 2,