breakout71/src/openUpgradesPicker.ts
2025-05-18 21:37:12 +02:00

307 lines
8.7 KiB
TypeScript

import { GameState, OptionId, PerkId } from "./types";
import {
catchRateBest,
catchRateGood,
choicePerGold,
choicePerSilver,
levelTimeBest,
levelTimeGood,
missesBest,
missesGood,
upPerGold,
upPerSilver,
} from "./pure_functions";
import { t } from "./i18n/i18n";
import { icons } from "./loadGameData";
import { requiredAsyncAlert } from "./asyncAlert";
import {
escapeAttribute,
getPossibleUpgrades,
levelsListHTMl,
max_levels,
pickedUpgradesHTMl,
renderMaxLevel,
upgradeLevelAndMaxDisplay,
} from "./game_utils";
import { getFirstUnlockable, getNearestUnlockHTML } from "./openScorePanel";
import { isOptionOn } from "./options";
import { getWorstFPSAndReset } from "./fps";
import {
getCurrentMaxCoins,
getSettingValue,
setSettingValue,
} from "./settings";
import { toast } from "./toast";
export async function openUpgradesPicker(gameState: GameState) {
if (gameState.perks.chill) return;
const catchRate =
gameState.levelCoughtCoins / (gameState.levelSpawnedCoins || 1);
let medals = [];
let upgradePoints = 1;
let extraChoices = 0;
let hasMedals = 0;
function challengeResult(
name: String,
description: String,
medal: "gold" | "silver" | "no",
) {
let choices = 0,
up = 0;
if (medal === "gold") {
choices += choicePerGold;
up += upPerGold;
} else if (medal === "silver") {
choices += choicePerSilver;
up += upPerSilver;
}
if (medal !== "no") {
hasMedals++;
}
extraChoices += choices;
upgradePoints += up;
medals.push(`<div class="upgrade" data-tooltip="${escapeAttribute(description)}">
${icons["icon:" + medal + "_medal"]}
<p>
<strong>${name}</strong><br/>
${
up || choices
? t("level_up.challenges.gain", { up, choices })
: t("level_up.challenges.no_gain")
}
</p>
</div>`);
}
challengeResult(
t("level_up.challenges.levelTime.name", {
value: Math.ceil(gameState.levelTime / 1000),
}),
t("level_up.challenges.levelTime.description", {
silver: levelTimeGood,
gold: levelTimeBest,
}),
(gameState.levelTime < levelTimeBest * 1000 && "gold") ||
(gameState.levelTime < levelTimeGood * 1000 && "silver") ||
"no",
);
challengeResult(
t("level_up.challenges.catchRateGood.name", {
value: Math.floor(catchRate * 100),
caught: gameState.levelCoughtCoins,
total: gameState.levelSpawnedCoins,
}),
t("level_up.challenges.catchRateGood.description", {
silver: catchRateGood,
gold: catchRateBest,
}),
(catchRate > catchRateBest / 100 && "gold") ||
(catchRate > catchRateGood / 100 && "silver") ||
"no",
);
challengeResult(
gameState.levelMisses
? t("level_up.challenges.levelMisses.name", {
value: gameState.levelMisses,
})
: t("level_up.challenges.levelMisses.none"),
t("level_up.challenges.levelMisses.description", {
silver: missesGood,
gold: missesBest,
}),
(gameState.levelMisses < missesBest && "gold") ||
(gameState.levelMisses < missesGood && "silver") ||
"no",
);
if (hasMedals == 0) {
medals.length = 0;
} else if (hasMedals == 1) {
medals.unshift(t("level_up.challenges.earned_medal", { count: hasMedals }));
} else {
medals.unshift(
t("level_up.challenges.earned_medal_plural", { count: hasMedals }),
);
}
let sorted = getPossibleUpgrades(gameState)
.map((u) => ({
...u,
score: Math.random() + (gameState.lastOffered[u.id] || 0),
}))
.sort((a, b) => a.score - b.score)
.filter((u) => gameState.perks[u.id] < u.max + gameState.perks.limitless);
let recommendation = settingsChangeRecommendations();
while (true && !gameState.perks.chill) {
// refresh the list if you pick extra one_more_choice
const offered = sorted.slice(
0,
3 + extraChoices + gameState.perks.one_more_choice,
);
offered.forEach((u) => {
dontOfferTooSoon(gameState, u.id);
});
const unlockable = getFirstUnlockable(gameState);
let unlockRelatedUpgradesOffered = 0;
const upgradesActions = offered.map((u) => {
let unlockHint = "";
let className = "";
if (isOptionOn("level_unlocks_hints")) {
if (unlockable?.forbidden?.includes(u.id) && !gameState.perks[u.id]) {
unlockRelatedUpgradesOffered++;
className += " forbidden";
unlockHint = t("level_up.forbidden", {
levelName: unlockable?.l.name || "",
});
}
if (unlockable?.required?.includes(u.id)) {
unlockRelatedUpgradesOffered++;
className += " required";
unlockHint = t("level_up.required", {
levelName: unlockable?.l.name || "",
});
}
}
return {
value: u.id,
disabled: gameState.perks[u.id] >= u.max + gameState.perks.limitless,
text:
u.name +
(gameState.perks[u.id]
? upgradeLevelAndMaxDisplay(u, gameState)
: ""),
icon: icons["icon:" + u.id],
help: u.help(gameState.perks[u.id] || 1),
tooltip: unlockHint + u.fullHelp(gameState.perks[u.id] || 1),
className,
actionLabel: gameState.perks[u.id] ? "upgrade" : "pick",
};
});
const choice = await requiredAsyncAlert<
PerkId | { changeSettings: Record<string, any> }
>({
title: t("level_up.title", {
level: gameState.currentLevel,
max: renderMaxLevel(gameState),
}),
content: [
t("level_up.upgrade_perks", {
coins: gameState.levelCoughtCoins,
count: upgradePoints,
}),
...upgradesActions,
levelsListHTMl(gameState, gameState.currentLevel),
unlockRelatedUpgradesOffered ? getNearestUnlockHTML(gameState) : "",
recommendation,
...medals,
pickedUpgradesHTMl(gameState),
`<div id="level-recording-container"></div>`,
],
});
if (applySettingsChangeReco(choice)) {
recommendation = "";
} else {
upgradePoints--;
gameState.perks[choice]++;
gameState.runStatistics.upgrades_picked++;
if (!upgradePoints) {
return;
}
}
}
}
export function dontOfferTooSoon(gameState: GameState, id: PerkId) {
gameState.lastOffered[id] = Math.round(Date.now() / 1000);
}
export function applySettingsChangeReco(choice: unknown) {
if (!choice) return;
if (typeof choice == "object" && "changeSettings" in choice) {
for (let key in choice.changeSettings) {
setSettingValue(key, choice.changeSettings[key]);
}
toast(t("settings.suggestions.applied"));
return true;
}
return false;
}
export function settingsChangeRecommendations() {
const { worstFPS, coinsForLag } = getWorstFPSAndReset();
const maxCoinsSetting = getSettingValue("max_coins", 2);
if (worstFPS > 55) return "";
if (coinsForLag > 200 && getCurrentMaxCoins() > 200) {
// Limit the coins
const limit = Math.floor(Math.log2(coinsForLag / 200));
if (limit < maxCoinsSetting) {
return {
icon: icons["icon:slow"],
text: t("settings.suggestions.reduce_coins", {
max: Math.pow(2, limit) * 200,
}),
value: {
changeSettings: { max_coins: limit },
},
};
}
}
if (isOptionOn("record"))
return {
icon: icons["icon:slow"],
text: t("settings.suggestions.record"),
value: {
changeSettings: { "breakout-settings-enable-record": false },
},
};
if (isOptionOn("basic")) return "";
if (
isOptionOn("smooth_lighting") ||
isOptionOn("precise_lighting") ||
!isOptionOn("probabilistic_lighting") ||
isOptionOn("contrast")
)
return {
icon: icons["icon:slow"],
text: t("settings.suggestions.simpler_lights"),
value: {
changeSettings: {
"breakout-settings-enable-smooth_lighting": false,
"breakout-settings-enable-precise_lighting": false,
"breakout-settings-enable-probabilistic_lighting": true,
"breakout-settings-enable-contrast": false,
},
},
};
if (isOptionOn("extra_bright"))
return {
icon: icons["icon:slow"],
text: t("settings.suggestions.reduce_brightness"),
value: {
changeSettings: { "breakout-settings-enable-extra_bright": false },
},
};
return {
icon: icons["icon:slow"],
text: t("settings.suggestions.basic_mode"),
value: {
changeSettings: { "breakout-settings-enable-basic": true },
},
};
}