breakout71/src/game_utils.ts
Renan LE CARO ca1c75a5a1 wip
2025-05-03 08:20:00 +02:00

347 lines
8.8 KiB
TypeScript

import {
Ball,
Coin,
GameState,
Level,
PerkId,
PerksMap,
Upgrade,
} from "./types";
import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { clamp } from "./pure_functions";
import { getSettingValue, getTotalScore } from "./settings";
import { isOptionOn } from "./options";
export function describeLevel(level: Level) {
let bricks = 0,
colors = new Set(),
bombs = 0;
level.bricks.forEach((color) => {
if (!color) return;
if (color === "black") {
bombs++;
return;
} else {
colors.add(color);
bricks++;
}
});
return t("unlocks.level_description", {
size: level.size,
bricks,
colors: colors.size,
bombs,
});
}
export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {};
arr.forEach((v) => (count[v] = (count[v] || 0) + 1));
// Object.values inline polyfill
const max = Math.max(...Object.keys(count).map((k) => count[k]));
return sample(Object.keys(count).filter((k) => count[k] == max));
}
export function sample<T>(arr: T[]): T {
return arr[Math.floor(arr.length * Math.random())];
}
export function sumOfValues(obj: { [key: string]: number } | undefined | null) {
if (!obj) return 0;
return Object.values(obj)?.reduce((a, b) => a + b, 0) || 0;
}
export const makeEmptyPerksMap = (upgrades: { id: PerkId }[]) => {
const p = {} as any;
upgrades.forEach((u) => (p[u.id] = 0));
return p as PerksMap;
};
export function brickCenterX(gameState: GameState, index: number) {
return (
gameState.offsetX +
((index % gameState.gridSize) + 0.5) * gameState.brickWidth
);
}
export function brickCenterY(gameState: GameState, index: number) {
return (Math.floor(index / gameState.gridSize) + 0.5) * gameState.brickWidth;
}
export function getRowColIndex(gameState: GameState, row: number, col: number) {
if (
row < 0 ||
col < 0 ||
row >= gameState.gridSize ||
col >= gameState.gridSize
)
return -1;
return row * gameState.gridSize + col;
}
export function getClosestBall(
gameState: GameState,
x: number,
y: number,
): Ball | null {
let closestBall: Ball | null = null;
let dist = 0;
gameState.balls.forEach((ball) => {
const d2 = (ball.x - x) * (ball.x - x) + (ball.y - y) * (ball.y - y);
if (d2 < dist || !closestBall) {
closestBall = ball;
dist = d2;
}
});
return closestBall;
}
export function getPossibleUpgrades(gameState: GameState) {
return upgrades
.filter((u) => getTotalScore() >= u.threshold)
.filter((u) => !u?.requires || gameState.perks[u?.requires]);
}
export function max_levels(gameState: GameState) {
if (gameState.creative) return 1;
return 7 + gameState.perks.extra_levels;
}
export function upgradeLevelAndMaxDisplay(
upgrade: Upgrade,
gameState: GameState,
) {
const lvl = gameState.perks[upgrade.id];
const max = upgrade.max + gameState.perks.limitless;
return levelAndMaxBadge(lvl, max);
}
export function levelAndMaxBadge(lvl: number, max: number) {
return ` <span class="level ${lvl < max ? "can-upgrade" : "capped"}"><span>${lvl}</span><span>${max}</span></span>`;
}
export function pickedUpgradesHTMl(gameState: GameState) {
const upgradesList = getPossibleUpgrades(gameState)
.filter((u) => gameState.perks[u.id])
.map((u) => {
const newMax = Math.max(0, u.max + gameState.perks.limitless);
const state = (gameState.perks[u.id] && 1) || (!newMax && 2) || 3;
const tooltip = escapeAttribute(u.fullHelp(gameState.perks[u.id] || 1));
return {
state,
html: `
<div class="upgrade ${["??", "used", "banned", "free"][state]}">
${icons["icon:" + u.id]}
<p data-tooltip="${tooltip}"
data-help-content="${tooltip}"
>
<strong>${u.name}</strong>
${upgradeLevelAndMaxDisplay(u, gameState)}
${u.help(gameState.perks[u.id] || 1)}
</p>
</div>
`,
};
})
.sort((a, b) => a.state - b.state)
.map((a) => a.html);
return ` <p>${t("score_panel.upgrades_picked")}</p>` + upgradesList.join("");
}
export function levelsListHTMl(
gameState: GameState,
currentLevelIndex: number,
) {
if (!gameState.perks.clairvoyant) return "";
if (gameState.creative) return "";
let list = "";
for (let i = 0; i < max_levels(gameState); i++) {
let level = gameState.runLevels[i % gameState.runLevels.length];
list += `<span style="opacity: ${i >= currentLevelIndex ? 1 : 0.2}" title="${level.name}">${icons[level.name]}</span>`;
}
return `<p>${t("score_panel.upcoming_levels")}</p><p>${list}</p>`;
}
export function currentLevelInfo(gameState: GameState) {
return gameState.level;
}
export function isPickyEatingPossible(gameState: GameState) {
return gameState.bricks.indexOf(gameState.ballsColor) !== -1;
}
export function reachRedRowIndex(gameState: GameState) {
if (!gameState.perks.reach) return -1;
const { size } = gameState.level;
let minY = -1,
maxY = -1,
maxYCount = -1;
for (let y = 0; y < size; y++)
for (let x = 0; x < size; x++)
if (gameState.bricks[x + y * size]) {
if (minY == -1) minY = y;
if (maxY < y) {
maxY = y;
maxYCount = 0;
}
if (maxY == y) maxYCount++;
}
if (maxY < 1) return -1;
if (maxY == minY) return -1;
if (maxYCount === size) return -1;
return maxY;
}
export function telekinesisEffectRate(gameState: GameState, ball: Ball) {
return (
(gameState.perks.telekinesis &&
ball.vy < 0 &&
clamp((ball.y / gameState.gameZoneHeight) * 1.1 + 0.1, 0, 1)) ||
0
);
}
export function yoyoEffectRate(gameState: GameState, ball: Ball) {
if (ball.vy < 0) return 0;
if (!gameState.perks.yoyo) return 0;
return (
((Math.abs(gameState.puckPosition - ball.x) / gameState.gameZoneWidth) *
gameState.perks.yoyo) /
2
);
}
export function findLast<T>(
arr: T[],
predicate: (item: T, index: number, array: T[]) => boolean,
) {
let i = arr.length;
while (--i)
if (predicate(arr[i], i, arr)) {
return arr[i];
}
}
export function distance2(
a: { x: number; y: number },
b: { x: number; y: number },
) {
return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
}
export function distanceBetween(
a: { x: number; y: number },
b: { x: number; y: number },
) {
return Math.sqrt(distance2(a, b));
}
export function defaultSounds() {
return {
aboutToPlaySound: {
wallBeep: { vol: 0, x: 0 },
comboIncreaseMaybe: { vol: 0, x: 0 },
comboDecrease: { vol: 0, x: 0 },
coinBounce: { vol: 0, x: 0 },
explode: { vol: 0, x: 0 },
lifeLost: { vol: 0, x: 0 },
coinCatch: { vol: 0, x: 0 },
plouf: { vol: 0, x: 0 },
colorChange: { vol: 0, x: 0 },
},
};
}
export function shouldPierceByColor(
gameState: GameState,
vhit: number | undefined,
hhit: number | undefined,
chit: number | undefined,
) {
if (!gameState.perks.pierce_color) return false;
if (
typeof vhit !== "undefined" &&
gameState.bricks[vhit] !== gameState.ballsColor
) {
return false;
}
if (
typeof hhit !== "undefined" &&
gameState.bricks[hhit] !== gameState.ballsColor
) {
return false;
}
if (
typeof chit !== "undefined" &&
gameState.bricks[chit] !== gameState.ballsColor
) {
return false;
}
return true;
}
export function isMovingWhilePassiveIncome(gameState: GameState) {
return !!(
gameState.lastPuckMove &&
gameState.perks.passive_income &&
gameState.lastPuckMove >
gameState.levelTime - (100 * gameState.perks.passive_income - 50)
);
}
export function getHighScore() {
try {
return parseInt(localStorage.getItem("breakout-3-hs-short") || "0");
} catch (e) {}
return 0;
}
export function highScoreText() {
if (getHighScore()) {
return t("main_menu.high_score", { score: getHighScore() });
}
return "";
}
export function getCoinRenderColor(gameState: GameState, coin: Coin) {
if (
gameState.perks.metamorphosis ||
isOptionOn("colorful_coins") ||
gameState.perks.sticky_coins ||
gameState.perks.rainbow
)
return coin.color;
return "#ffd300";
}
export function getCornerOffset(gameState: GameState) {
return (
(gameState.levelTime
? gameState.perks.corner_shot * gameState.brickWidth
: 0) -
gameState.perks.unbounded * gameState.brickWidth
);
}
export const isInWebView = !!window.location.href.includes("isInWebView=true");
export function hoursSpentPlaying() {
try {
const timePlayed = getSettingValue("breakout_71_total_play_time", 0);
return Math.floor(timePlayed / 1000 / 60 / 60);
} catch (e) {
return 0;
}
}
export function escapeAttribute(str: String) {
return str
.replace(/&/gi, "&amp;")
.replace(/</gi, "&lt;")
.replace(/"/gi, "&quot;")
.replace(/'/gi, "&#39;");
}