Wip : adventure mode

This commit is contained in:
Renan LE CARO 2025-03-26 14:04:54 +01:00
parent 395968bc52
commit 6cf8fabf16
14 changed files with 457 additions and 114 deletions

View file

@ -1,26 +1,110 @@
import { GameState } from "./types";
import {GameState, PerksMap} from "./types";
import {sample, sumOfValues} from "./game_utils";
import {allLevels, icons, upgrades} from "./loadGameData";
import {t} from "./i18n/i18n";
import {hashCode} from "./getLevelBackground";
import {requiredAsyncAlert} from "./asyncAlert";
const MAX_DIFFICULTY = 3+4
export async function openAdventureRunUpgradesPicker(gameState: GameState) {
let options = 3;
const catchRate =
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1);
let maxDifficulty = 3;
if (gameState.levelWallBounces == 0) {
options++;
}
if (gameState.levelTime < 30 * 1000) {
options++;
}
if (catchRate === 1) {
options++;
}
if (gameState.levelMisses === 0) {
options++;
}
const catchRate =
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1);
const choices = [];
for (let difficulty = 0; difficulty < options; difficulty++) {
choices.push({});
}
if (gameState.levelWallBounces == 0) {
maxDifficulty++;
}
if (gameState.levelTime < 30 * 1000) {
maxDifficulty++;
}
if (catchRate === 1) {
maxDifficulty++;
}
if (gameState.levelMisses === 0) {
maxDifficulty++;
}
let actions = range(0, maxDifficulty).map(difficulty => getPerksForPath(gameState.score, gameState.currentLevel, gameState.seed, gameState.adventurePath, gameState.perks, difficulty))
return requiredAsyncAlert({
title: 'Choose your next step',
text: 'Click one of the options below to continue',
actions,
})
}
function getPerksForPath(score: number, currentLevel: number, seed: string, path: string, basePerks: PerksMap, difficulty: number) {
const hashSeed = seed + path
let cost = (1 + difficulty) * Math.pow(1.5, currentLevel) * 10
if (!difficulty && cost > score) {
cost = score
}
const levels = allLevels
.sort((a, b) => hashCode(hashSeed + a.name) - hashCode(hashSeed + b.name))
.slice(0, MAX_DIFFICULTY)
.sort((a,b)=>a.size-b.size)
let level = levels[difficulty]
let text = level.name + ' $' + cost, help = []
let perks = {}
// TODO exclude irrelevant perks
upgrades
.filter((u) => !u?.requires || basePerks[u?.requires])
.filter(u => basePerks[u.id] < u.max)
.sort((a, b) => hashCode(hashSeed + difficulty+a.id) - hashCode(hashSeed + difficulty+b.id))
.slice(0, difficulty+1)
.forEach(u => {
perks[u.id] = basePerks[u.id] + 1
help.push(u.name +
(basePerks[u.id]
? t("level_up.upgrade_perk_to_level", {
level: basePerks[u.id] + 1,
})
: ""))
})
let totalPerksValue = sumOfValues({...basePerks, ...perks})
let targetPerks = 10 + difficulty * 3
let toRemove = Math.max(0, totalPerksValue - targetPerks)
while (toRemove) {
const possibleDowngrades = Object.keys(basePerks).filter(
k => !perks[k] && basePerks[k] > 0
)
if (!possibleDowngrades.length) {
break
}
const downGraded = sample(possibleDowngrades)
perks[downGraded] = basePerks[downGraded] - 1
if (!perks[downGraded]) {
help.push(t('level_up.perk_loss') + upgrades.find(u => u.id == downGraded)?.help(1))
} else {
help.push(t('level_up.downgrade') + upgrades.find(u => u.id == downGraded)?.help(perks[downGraded]))
}
toRemove--
}
return {
value: {
cost,
level,
perks,
difficulty
},
icon: icons[level.name],
disabled: cost > score,
text, help: help.join('/') || 'No change to perks',
}
}
function range(start: number, end: number): number[] {
const result = []
for (let i = start; i < end; i++) result.push(i)
return result
}

View file

@ -25,6 +25,16 @@ closeModaleButton.title = t("play.close_modale_window_tooltip");
let lastClickedItemIndex = -1;
export function requiredAsyncAlert<t>(p: {
title?: string;
text?: string;
actions?: AsyncAlertAction<t>[];
textAfterButtons?: string;
actionsAsGrid?: boolean;
}):Promise<t>{
return asyncAlert({...p, allowClose:false})
}
export async function asyncAlert<t>({
title,
text,

View file

@ -1053,4 +1053,4 @@
"svg": null,
"color": ""
}
]
]

View file

@ -58,7 +58,7 @@ import {
alertsOpen,
asyncAlert,
AsyncAlertAction,
closeModal,
closeModal, requiredAsyncAlert,
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options";
import { hashCode } from "./getLevelBackground";
@ -262,7 +262,7 @@ export async function openShortRunUpgradesPicker(gameState: GameState) {
t("level_up.compliment_good")) ||
t("level_up.compliment_advice");
const upgradeId = (await asyncAlert<PerkId | "reroll">({
const upgradeId = await requiredAsyncAlert<PerkId | "reroll">({
title:
t("level_up.pick_upgrade_title") +
(repeats ? " (" + (repeats + 1) + ")" : ""),
@ -283,9 +283,8 @@ export async function openShortRunUpgradesPicker(gameState: GameState) {
<p>${levelsListHTMl(gameState)}</p>
`,
allowClose: false,
textAfterButtons,
})) as PerkId;
}) ;
if (upgradeId === "reroll") {
repeats++;
@ -533,7 +532,7 @@ export async function openMainMenu() {
},
},
// premiumMenuEntry(gameState),
premiumMenuEntry(gameState),
{
text: t("main_menu.settings_title"),
help: t("main_menu.settings_help"),
@ -991,8 +990,7 @@ export function restart(params: RunParams) {
}
restart(
window.location.search.includes("stressTest")
? {
(window.location.search.includes("stressTest") && {
level: "Bird",
perks: {
sapper: 10,
@ -1001,13 +999,19 @@ restart(
pierce_color: 1,
pierce: 20,
multiball: 6,
base_combo: 100,
base_combo: 7,
telekinesis: 2,
yoyo: 2,
metamorphosis: 1,
implosions: 1,
},
}
: {},
}) ||(window.location.search.includes("adventure") && {
adventure:true,
perks: {
pierce:5
},
}) || {},
);
tick();

View file

@ -492,16 +492,16 @@ export function pickRandomUpgrades(gameState: GameState, count: number) {
let list = getPossibleUpgrades(gameState)
.map((u) => ({
...u,
score: Math.random() + (gameState.lastOffered[u.id] || 0),
score: gameState.isAdventureMode ? 0:Math.random() + (gameState.lastOffered[u.id] || 0),
}))
.sort((a, b) => a.score - b.score)
.filter((u) => gameState.perks[u.id] < u.max)
.slice(0, count)
.sort((a, b) => (a.id > b.id ? 1 : -1));
list.forEach((u) => {
dontOfferTooSoon(gameState, u.id);
});
list.forEach((u) => {
dontOfferTooSoon(gameState, u.id);
});
return list.map((u) => ({
text:
@ -576,7 +576,16 @@ export async function setLevel(gameState: GameState, l: number) {
stopRecording();
if (l > 0) {
if (gameState.isCreativeModeRun) {
await openAdventureRunUpgradesPicker(gameState);
const { cost,
level,
perks,difficulty} = await openAdventureRunUpgradesPicker(gameState);
gameState.score-=cost
gameState.runLevels[l] = level
gameState.adventurePath += difficulty
Object.assign(gameState.perks, perks)
} else {
await openShortRunUpgradesPicker(gameState);
}

View file

@ -2,7 +2,7 @@ import {
getMajorityValue,
makeEmptyPerksMap,
sample,
sumOfKeys,
sumOfValues,
} from "./game_utils";
describe("getMajorityValue", () => {
@ -31,16 +31,16 @@ describe("sample", () => {
});
describe("sumOfKeys", () => {
it("returns the sum of the keys of an array", () => {
expect(sumOfKeys({ a: 1, b: 2 })).toEqual(3);
expect(sumOfValues({ a: 1, b: 2 })).toEqual(3);
});
it("returns 0 for an empty object", () => {
expect(sumOfKeys({})).toEqual(0);
expect(sumOfValues({})).toEqual(0);
});
it("returns 0 for undefined", () => {
expect(sumOfKeys(undefined)).toEqual(0);
expect(sumOfValues(undefined)).toEqual(0);
});
it("returns 0 for null", () => {
expect(sumOfKeys(null)).toEqual(0);
expect(sumOfValues(null)).toEqual(0);
});
});
describe("makeEmptyPerksMap", () => {

View file

@ -14,7 +14,7 @@ export function sample<T>(arr: T[]): T {
return arr[Math.floor(arr.length * Math.random())];
}
export function sumOfKeys(obj: { [key: string]: number } | undefined | null) {
export function sumOfValues(obj: { [key: string]: number } | undefined | null) {
if (!obj) return 0;
return Object.values(obj)?.reduce((a, b) => a + b, 0) || 0;
}

View file

@ -512,6 +512,36 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>downgrade</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>perk_loss</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>pick_upgrade_title</name>
<description/>

View file

@ -30,6 +30,8 @@
"level_up.compliment_advice": "Try to catch all coins, never miss the bricks, never hit the walls/ceiling or clear the level under 30s to gain additional choices and upgrades.",
"level_up.compliment_good": "Well done !",
"level_up.compliment_perfect": "Impressive, keep it up !",
"level_up.downgrade": "Downgrade :",
"level_up.perk_loss": "Perk lost : ",
"level_up.pick_upgrade_title": "Pick an upgrade",
"level_up.plus_one_choice": "(+1 re-roll)",
"level_up.plus_one_upgrade": "(+1 upgrade and +1 re-roll)",

View file

@ -30,6 +30,8 @@
"level_up.compliment_advice": "Essayez d'attraper toutes les pièces, de ne jamais rater les briques, de ne pas toucher les murs ou de terminer le niveau en moins de 30 secondes pour obtenir des choix supplémentaires et des améliorations.",
"level_up.compliment_good": "Bravo !",
"level_up.compliment_perfect": "Impressionnant, continuez comme ça !",
"level_up.downgrade": "",
"level_up.perk_loss": "",
"level_up.pick_upgrade_title": "Choisir une amélioration",
"level_up.plus_one_choice": "(+1 re-roll)",
"level_up.plus_one_upgrade": "(+1 amélioration et +1 re-roll)",

View file

@ -5,7 +5,7 @@ import {
defaultSounds,
getPossibleUpgrades,
makeEmptyPerksMap,
sumOfKeys,
sumOfValues,
} from "./game_utils";
import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
import { isOptionOn } from "./options";
@ -73,7 +73,7 @@ export function newGameState(params: RunParams): GameState {
coinSize: 14,
puckHeight: 20,
totalScoreAtRunStart,
isCreativeModeRun: sumOfKeys(perks) > 1,
isCreativeModeRun: sumOfValues(perks) > 1,
pauseUsesDuringRun: 0,
keyboardPuckSpeed: 0,
lastTick: performance.now(),
@ -108,7 +108,7 @@ export function newGameState(params: RunParams): GameState {
};
resetBalls(gameState);
if (!sumOfKeys(gameState.perks)) {
if (!sumOfValues(gameState.perks)) {
const giftable = getPossibleUpgrades(gameState).filter((u) => u.giftable);
const randomGift =
(isOptionOn("easy") && "slow_down") ||

View file

@ -583,6 +583,7 @@ export const rawUpgrades = [
giftable: false,
id: "clairvoyant",
max: 1,
// TODO update for adventure mode
name: t("upgrades.clairvoyant.name"),
help: (lvl: number) => t("upgrades.clairvoyant.help"),
fullHelp: t("upgrades.clairvoyant.fullHelp"),