mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-23 13:36:15 -04:00
Wip : adventure mode
This commit is contained in:
parent
395968bc52
commit
6cf8fabf16
14 changed files with 457 additions and 114 deletions
126
src/adventure.ts
126
src/adventure.ts
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -1053,4 +1053,4 @@
|
|||
"svg": null,
|
||||
"color": ""
|
||||
}
|
||||
]
|
||||
]
|
24
src/game.ts
24
src/game.ts
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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") ||
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue