[Daily] Daily standardization (#3776)

* Disable Luck in Daily Runs

If the Game Mode is Daily Run, the player's Luck is set to 0, and the Luck value is hidden.

* Give free map in daily

Adds a Map to the player's pool of starting items for Daily Runs.

* Disable Eviolite in Daily Runs

Disables Eviolite spawning in Daily Run mode.

* Write shop test and add new overrides

Adds new overrides that allow you to force content to be locked or unlocked
These overrides were also added to the OverridesHelper to make them available to tests

Adds a new check function for content unlocks, which returns `true` if it is overrode to be unlocked, `false` if it is overrode to be locked, and the unlock data mapped to a Boolean otherwise

All existing checks (other than the ones that involve actually unlocking things) for unlockables have been changed to use this

Added a pair of new exporting booleans, specifically for my test, that check if Eviolite or Mini Black Hole are in the loot table

* Prevent shinies from altering runs

Places variant rolls inside of an ExecuteWithSeedOffset block, using the current floor's RNG seed as the seed and the Pokémon's ID as the offset.

---------

Co-authored-by: Leo Kim <47556641+KimJeongSun@users.noreply.github.com>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>
This commit is contained in:
RedstonewolfX 2024-09-26 04:39:59 -04:00 committed by GitHub
parent 6520a74cb4
commit 06331ccdf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 240 additions and 16 deletions

View File

@ -1669,7 +1669,11 @@ export default class BattleScene extends SceneBase {
this.scoreText.setVisible(this.gameMode.isDaily);
}
updateAndShowText(duration: integer): void {
/**
* Displays the current luck value.
* @param duration The time for this label to fade in, if it is not already visible.
*/
updateAndShowText(duration: number): void {
const labels = [ this.luckLabelText, this.luckText ];
labels.forEach(t => t.setAlpha(0));
const luckValue = getPartyLuckValue(this.getParty());

View File

@ -1914,10 +1914,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!this.shiny || (!variantData.hasOwnProperty(variantDataIndex) && !variantData.hasOwnProperty(this.species.speciesId))) {
return 0;
}
const rand = Utils.randSeedInt(10);
if (rand >= 4) {
const rand = new Utils.NumberHolder(0);
this.scene.executeWithSeedOffset(() => {
rand.value = Utils.randSeedInt(10);
}, this.id, this.scene.waveSeed);
if (rand.value >= 4) {
return 0; // 6/10
} else if (rand >= 1) {
} else if (rand.value >= 1) {
return 1; // 3/10
} else {
return 2; // 1/10

View File

@ -1719,7 +1719,8 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.FORM_CHANGE_ITEM, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 6, 24),
new WeightedModifierType(modifierTypes.AMULET_COIN, skipInLastClassicWaveOrDefault(3)),
new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => {
if (!party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.unlocks[Unlockables.EVIOLITE]) {
const { gameMode, gameData } = party[0].scene;
if (gameMode.isDaily || (!gameMode.isFreshStartChallenge() && gameData.isUnlocked(Unlockables.EVIOLITE))) {
return party.some(p => ((p.getSpeciesForm(true).speciesId in pokemonEvolutions) || (p.isFusion() && (p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)))
&& !p.getHeldItems().some(i => i instanceof Modifiers.EvolutionStatBoosterModifier) && !p.isMax()) ? 10 : 0;
}
@ -1804,7 +1805,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (party: Pokemon[], rerollCount: integer) =>
!party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5),
new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24),
new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, (party: Pokemon[]) => (!party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) ? 1 : 0, 1),
new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, (party: Pokemon[]) => (party[0].scene.gameMode.isDaily || (!party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE))) ? 1 : 0, 1),
].map(m => {
m.setTier(ModifierTier.MASTER); return m;
})
@ -1996,9 +1997,16 @@ export function getModifierPoolForType(poolType: ModifierPoolType): ModifierPool
}
const tierWeights = [ 768 / 1024, 195 / 1024, 48 / 1024, 12 / 1024, 1 / 1024 ];
/**
* Allows a unit test to check if an item exists in the Modifier Pool. Checks the pool directly, rather than attempting to reroll for the item.
*/
export const itemPoolChecks: Map<ModifierTypeKeys, boolean | undefined> = new Map();
export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: ModifierPoolType, rerollCount: integer = 0) {
const pool = getModifierPoolForType(poolType);
itemPoolChecks.forEach((v, k) => {
itemPoolChecks.set(k, false);
});
const ignoredIndexes = {};
const modifierTableData = {};
@ -2035,6 +2043,9 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod
ignoredIndexes[t].push(i++);
return total;
}
if (itemPoolChecks.has(modifierType.modifierType.id as ModifierTypeKeys)) {
itemPoolChecks.set(modifierType.modifierType.id as ModifierTypeKeys, true);
}
thresholds.set(total, i++);
return total;
}, 0);
@ -2437,10 +2448,22 @@ export class ModifierTypeOption {
}
}
/**
* Calculates the team's luck value.
* @param party The player's party.
* @returns A number between 0 and 14 based on the party's total luck value, or a random number between 0 and 14 if the player is in Daily Run mode.
*/
export function getPartyLuckValue(party: Pokemon[]): integer {
if (party[0].scene.gameMode.isDaily) {
const DailyLuck = new Utils.NumberHolder(0);
party[0].scene.executeWithSeedOffset(() => {
DailyLuck.value = Utils.randSeedInt(15); // Random number between 0 and 14
}, 0, party[0].scene.seed);
return DailyLuck.value;
}
const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() : 0)
.reduce((total: integer, value: integer) => total += value, 0), 0, 14);
return luck || 0;
return luck ?? 0;
}
export function getLuckString(luckValue: integer): string {

View File

@ -12,6 +12,7 @@ import { type PokeballCounts } from "./battle-scene";
import { Gender } from "./data/gender";
import { Variant } from "./data/variant";
import { type ModifierOverride } from "./modifier/modifier-type";
import { Unlockables } from "./system/unlockables";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -70,8 +71,10 @@ class DefaultOverrides {
[PokeballType.MASTER_BALL]: 0,
},
};
/** Forces an item to be UNLOCKED */
readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = [];
/** Set to `true` to show all tutorials */
readonly BYPASS_TUTORIAL_SKIP: boolean = false;
readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false;
// ----------------
// PLAYER OVERRIDES

View File

@ -76,7 +76,8 @@ export class TitlePhase extends Phase {
this.scene.ui.clearText();
this.end();
};
if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) {
const { gameData } = this.scene;
if (gameData.isUnlocked(Unlockables.ENDLESS_MODE)) {
const options: OptionSelectItem[] = [
{
label: GameMode.getModeName(GameModes.CLASSIC),
@ -100,7 +101,7 @@ export class TitlePhase extends Phase {
}
}
];
if (this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) {
if (gameData.isUnlocked(Unlockables.SPLICED_ENDLESS_MODE)) {
options.push({
label: GameMode.getModeName(GameModes.SPLICED_ENDLESS),
handler: () => {
@ -220,6 +221,7 @@ export class TitlePhase extends Phase {
const modifiers: Modifier[] = Array(3).fill(null).map(() => modifierTypes.EXP_SHARE().withIdFromFunc(modifierTypes.EXP_SHARE).newModifier())
.concat(Array(3).fill(null).map(() => modifierTypes.GOLDEN_EXP_CHARM().withIdFromFunc(modifierTypes.GOLDEN_EXP_CHARM).newModifier()))
.concat([modifierTypes.MAP().withIdFromFunc(modifierTypes.MAP).newModifier()])
.concat(getDailyRunStarterModifiers(party))
.filter((m) => m !== null);

View File

@ -372,6 +372,18 @@ export class GameData {
};
}
/**
* Checks if an `Unlockable` has been unlocked.
* @param unlockable The Unlockable to check
* @returns `true` if the player has unlocked this `Unlockable` or an override has enabled it
*/
public isUnlocked(unlockable: Unlockables): boolean {
if (Overrides.ITEM_UNLOCK_OVERRIDE.includes(unlockable)) {
return true;
}
return this.unlocks[unlockable];
}
public saveSystem(): Promise<boolean> {
return new Promise<boolean>(resolve => {
this.scene.ui.savingIcon.show();

View File

@ -1,5 +1,12 @@
import { MapModifier } from "#app/modifier/modifier";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import GameManager from "./utils/gameManager";
import { Moves } from "#app/enums/moves";
import { Biome } from "#app/enums/biome";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
//const TIMEOUT = 20 * 1000;
describe("Daily Mode", () => {
let phaserGame: Phaser.Game;
@ -28,5 +35,66 @@ describe("Daily Mode", () => {
expect(pkm.level).toBe(20);
expect(pkm.moveset.length).toBeGreaterThan(0);
});
expect(game.scene.getModifiers(MapModifier).length).toBeGreaterThan(0);
});
});
describe("Shop modifications", async () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.startingWave(9)
.startingBiome(Biome.ICE_CAVE) // Will lead to Snowy Forest with randomly generated weather
.battleType("single")
.startingLevel(100) // Avoid levelling up
.enemyLevel(1000) // Avoid opponent dying before game.doKillOpponents()
.disableTrainerWaves()
.moveset([Moves.KOWTOW_CLEAVE])
.enemyMoveset(Moves.SPLASH);
game.modifiers
.addCheck("EVIOLITE")
.addCheck("MINI_BLACK_HOLE");
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
game.modifiers.clearChecks();
});
it("should not have Eviolite and Mini Black Hole available in Classic if not unlocked", async () => {
await game.classicMode.startBattle();
game.move.select(Moves.KOWTOW_CLEAVE);
await game.phaseInterceptor.to("DamagePhase");
await game.doKillOpponents();
await game.phaseInterceptor.to("BattleEndPhase");
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
expect(game.scene.ui.getHandler()).toBeInstanceOf(ModifierSelectUiHandler);
game.modifiers
.testCheck("EVIOLITE", false)
.testCheck("MINI_BLACK_HOLE", false);
});
});
it("should have Eviolite and Mini Black Hole available in Daily", async () => {
await game.dailyMode.startBattle();
game.move.select(Moves.KOWTOW_CLEAVE);
await game.phaseInterceptor.to("DamagePhase");
await game.doKillOpponents();
await game.phaseInterceptor.to("BattleEndPhase");
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
expect(game.scene.ui.getHandler()).toBeInstanceOf(ModifierSelectUiHandler);
game.modifiers
.testCheck("EVIOLITE", true)
.testCheck("MINI_BLACK_HOLE", true);
});
});
});

View File

@ -2,6 +2,7 @@ import { GameMode, GameModes, getGameMode } from "#app/game-mode";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import * as Utils from "../utils";
import GameManager from "./utils/gameManager";
describe("game-mode", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
@ -12,6 +13,7 @@ describe("game-mode", () => {
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
beforeEach(() => {

View File

@ -1,9 +1,13 @@
import { Species } from "#app/enums/species";
import { GameModes } from "#app/game-mode";
import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler";
import { Mode } from "#app/ui/ui";
import { Biome } from "#enums/biome";
import { Button } from "#enums/buttons";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { MockClock } from "#test/utils/mocks/mockClock";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { Moves } from "#app/enums/moves";
import { Biome } from "#app/enums/biome";
describe("Reload", () => {
let phaserGame: Phaser.Game;
@ -50,6 +54,13 @@ describe("Reload", () => {
game.move.select(Moves.KOWTOW_CLEAVE);
await game.phaseInterceptor.to("DamagePhase");
await game.doKillOpponents();
game.onNextPrompt("SelectBiomePhase", Mode.OPTION_SELECT, () => {
(game.scene.time as MockClock).overrideDelay = null;
const optionSelectUiHandler = game.scene.ui.getHandler() as OptionSelectUiHandler;
game.scene.time.delayedCall(1010, () => optionSelectUiHandler.processInput(Button.ACTION));
game.endPhase();
(game.scene.time as MockClock).overrideDelay = 1;
});
await game.toNextWave();
expect(game.phaseInterceptor.log).toContain("NewBiomeEncounterPhase");

View File

@ -47,6 +47,7 @@ import { MoveHelper } from "./helpers/moveHelper";
import { OverridesHelper } from "./helpers/overridesHelper";
import { SettingsHelper } from "./helpers/settingsHelper";
import { ReloadHelper } from "./helpers/reloadHelper";
import { ModifierHelper } from "./helpers/modifiersHelper";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
@ -71,6 +72,7 @@ export default class GameManager {
public readonly challengeMode: ChallengeModeHelper;
public readonly settings: SettingsHelper;
public readonly reload: ReloadHelper;
public readonly modifiers: ModifierHelper;
/**
* Creates an instance of GameManager.
@ -93,6 +95,7 @@ export default class GameManager {
this.challengeMode = new ChallengeModeHelper(this);
this.settings = new SettingsHelper(this);
this.reload = new ReloadHelper(this);
this.modifiers = new ModifierHelper(this);
// Disables Mystery Encounters on all tests (can be overridden at test level)
this.override.mysteryEncounterChance(0);

View File

@ -0,0 +1,58 @@
import { expect } from "vitest";
import { GameManagerHelper } from "./gameManagerHelper";
import { itemPoolChecks, ModifierTypeKeys } from "#app/modifier/modifier-type";
export class ModifierHelper extends GameManagerHelper {
/**
* Adds a Modifier to the list of modifiers to check for.
*
* Note that all modifiers are updated during the start of `SelectModifierPhase`.
* @param modifier The Modifier to add.
* @returns `this`
*/
addCheck(modifier: ModifierTypeKeys): this {
itemPoolChecks.set(modifier, undefined);
return this;
}
/**
* `get`s a value from the `itemPoolChecks` map.
*
* If the item is in the Modifier Pool, and the player can get it, will return `true`.
*
* If the item is *not* in the Modifier Pool, will return `false`.
*
* If a `SelectModifierPhase` has not occurred, and we do not know if the item is in the Modifier Pool or not, will return `undefined`.
* @param modifier
* @returns
*/
getCheck(modifier: ModifierTypeKeys): boolean | undefined {
return itemPoolChecks.get(modifier);
}
/**
* `expect`s a Modifier `toBeTruthy` (in the Modifier Pool) or `Falsy` (unobtainable on this floor). Use during a test.
*
* Note that if a `SelectModifierPhase` has not been run yet, these values will be `undefined`, and the check will fail.
* @param modifier The modifier to check.
* @param expectToBePreset Whether the Modifier should be in the Modifier Pool. Set to `false` to expect it to be absent instead.
* @returns `this`
*/
testCheck(modifier: ModifierTypeKeys, expectToBePreset: boolean): this {
if (expectToBePreset) {
expect(itemPoolChecks.get(modifier)).toBeTruthy();
}
expect(itemPoolChecks.get(modifier)).toBeFalsy();
return this;
}
/** Removes all modifier checks. @returns `this` */
clearChecks() {
itemPoolChecks.clear();
return this;
}
private log(...params: any[]) {
console.log("Modifiers:", ...params);
}
}

View File

@ -10,6 +10,8 @@ import { ModifierOverride } from "#app/modifier/modifier-type";
import Overrides from "#app/overrides";
import { vi } from "vitest";
import { GameManagerHelper } from "./gameManagerHelper";
import { Unlockables } from "#app/system/unlockables";
import { Variant } from "#app/data/variant";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -300,6 +302,17 @@ export class OverridesHelper extends GameManagerHelper {
return this;
}
/**
* Gives the player access to an Unlockable.
* @param unlockable The Unlockable(s) to enable.
* @returns `this`
*/
enableUnlockable(unlockable: Unlockables[]) {
vi.spyOn(Overrides, "ITEM_UNLOCK_OVERRIDE", "get").mockReturnValue(unlockable);
this.log("Temporarily unlocked the following content: ", unlockable);
return this;
}
/**
* Override the items rolled at the end of a battle
* @param items the items to be rolled
@ -311,6 +324,25 @@ export class OverridesHelper extends GameManagerHelper {
return this;
}
/**
* Override player shininess
* @param shininess Whether the player's Pokemon should be shiny.
*/
shinyLevel(shininess: boolean): this {
vi.spyOn(Overrides, "SHINY_OVERRIDE", "get").mockReturnValue(shininess);
this.log(`Set player Pokemon as ${shininess ? "" : "not "}shiny!`);
return this;
}
/**
* Override player shiny variant
* @param variant The player's shiny variant.
*/
variantLevel(variant: Variant): this {
vi.spyOn(Overrides, "VARIANT_OVERRIDE", "get").mockReturnValue(variant);
this.log(`Set player Pokemon's shiny variant to ${variant}!`);
return this;
}
/**
* Override the enemy (Pokemon) to have the given amount of health segments
* @param healthSegments the number of segments to give

View File

@ -43,6 +43,7 @@ import { UnavailablePhase } from "#app/phases/unavailable-phase";
import { VictoryPhase } from "#app/phases/victory-phase";
import { PartyHealPhase } from "#app/phases/party-heal-phase";
import UI, { Mode } from "#app/ui/ui";
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
import {
MysteryEncounterBattlePhase,
MysteryEncounterOptionSelectedPhase,
@ -122,6 +123,7 @@ export default class PhaseInterceptor {
[EndEvolutionPhase, this.startPhase],
[LevelCapPhase, this.startPhase],
[AttemptRunPhase, this.startPhase],
[SelectBiomePhase, this.startPhase],
[MysteryEncounterPhase, this.startPhase],
[MysteryEncounterOptionSelectedPhase, this.startPhase],
[MysteryEncounterBattlePhase, this.startPhase],
@ -346,7 +348,8 @@ export default class PhaseInterceptor {
console.log("setMode", `${Mode[mode]} (=${mode})`, args);
const ret = this.originalSetMode.apply(instance, [mode, ...args]);
if (!this.phases[currentPhase.constructor.name]) {
throw new Error(`missing ${currentPhase.constructor.name} in phaseInterceptor PHASES list`);
throw new Error(`missing ${currentPhase.constructor.name} in phaseInterceptor PHASES list --- Add it to PHASES inside of /test/utils/phaseInterceptor.ts`);
}
if (this.phases[currentPhase.constructor.name].endBySetMode) {
this.inProgress?.callback();

View File

@ -74,11 +74,11 @@ const tutorialHandlers = {
* @returns a promise with result `true` if the tutorial was run and finished, `false` otherwise
*/
export async function handleTutorial(scene: BattleScene, tutorial: Tutorial): Promise<boolean> {
if (!scene.enableTutorials && !Overrides.BYPASS_TUTORIAL_SKIP) {
if (!scene.enableTutorials && !Overrides.BYPASS_TUTORIAL_SKIP_OVERRIDE) {
return false;
}
if (scene.gameData.getTutorialFlags()[tutorial] && !Overrides.BYPASS_TUTORIAL_SKIP) {
if (scene.gameData.getTutorialFlags()[tutorial] && !Overrides.BYPASS_TUTORIAL_SKIP_OVERRIDE) {
return false;
}