Compare commits

...

41 Commits

Author SHA1 Message Date
RedstonewolfX 2e83236ee7
Merge 9062925cf8 into 51bb80cb66 2024-09-18 23:50:51 -07:00
MokaStitcher 51bb80cb66
[Bug][UI] Fix scrolling UIs not resetting properly and add Scrollbars (#4312)
* [bug] fix scrollable elements not resetting properly

* [ui] add wrap around and scrolling bar to the achievements menu

* [ui] add scrollbar to the settings
2024-09-18 19:53:30 -04:00
RedstonewolfX 9062925cf8 Almost there!! 2024-09-18 16:02:36 -04:00
RedstonewolfX 696a52fe15 Rename test 2024-09-18 14:58:39 -04:00
RedstonewolfX 4a27252231 Delete a skipped test, and skip a nonfunctional one
The test I added a skip statement to is just there as a second possible method of making these last tests
2024-09-18 14:58:24 -04:00
RedstonewolfX 752bd4f017 I might have done it
the tests didn't fail
2024-09-18 14:55:03 -04:00
RedstonewolfX c6c489b289
Merge branch 'beta' into daily-standardization 2024-09-18 14:25:45 -04:00
RedstonewolfX 21315a3664 Push progress so I can sync to beta 2024-09-18 14:25:22 -04:00
RedstonewolfX 029dfa4b3b Update reload.test.ts
Thanks to DayKev again for these fixes
2024-09-18 11:24:08 -04:00
flx-sta 6cfc6f9341
Merge branch 'beta' into daily-standardization 2024-09-17 19:14:00 -07:00
RedstonewolfX 9c391f92bd
Merge pull request #1 from pagefaultgames/main
[Bug] Fix Dire Hit & System Data Conversion Failure (#4282)
2024-09-17 20:24:09 -04:00
NightKev fa6b754dec
Merge branch 'beta' into daily-standardization 2024-09-17 05:38:18 -07:00
MokaStitcher 7ea608fb8a
[Bug] Fix Dire Hit & System Data Conversion Failure (#4282)
Co-authored-by: xsn34kzx <xsn34kzx@gmail.com>
2024-09-16 10:56:55 +01:00
Jannik Tappert 808b15bd94
Merge branch 'beta' into daily-standardization 2024-09-15 22:36:03 +02:00
RedstonewolfX a9b6359e34 Tests still broken
what else is new

god i hate tests

go away eslinter nobody likes you
2024-09-12 21:14:20 -04:00
RedstonewolfX 0114d795a5 Tests
I forgot what desc I wrote this morning but here you go

Still working on this, but will submit now to show my work
2024-09-12 19:35:04 -04:00
RedstonewolfX 8fdce2405c
Merge branch 'pagefaultgames:main' into daily-standardization 2024-09-12 10:46:23 -04:00
RedstonewolfX 64861447cb
Merge branch 'beta' into daily-standardization 2024-09-12 10:44:46 -04:00
Frederico Santos 801b0a66f7 Readded vouchers to original weights 2024-09-12 01:13:22 +01:00
Frederico Santos 103c87ec3b Undo egg skip event 2024-09-12 01:09:44 +01:00
NightKev 6373a86011 Fix tests 2024-09-01 01:13:37 -07:00
NightKev 2a2ef7d50c
Merge branch 'beta' into daily-standardization 2024-08-31 17:41:27 -07:00
RedstonewolfX b71ddda191 (Temporary) Implement startBattle fix 2024-08-27 18:52:07 -04:00
RedstonewolfX 49036f9522 More testing attempts
please help me
2024-08-27 18:28:27 -04:00
RedstonewolfX 3e65888ff1 Update overrides
Relocated the "isUnlocked" function to gameData since it requires a reference to it in order to run

Removed unnecessary override that disables unlocked items (and its associated overridesHelper statement)
2024-08-27 17:54:09 -04:00
RedstonewolfX d7263b6677
Update src/modifier/modifier-type.ts
Remove unused code I forgot to delete

Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>
2024-08-27 14:09:48 -04:00
RedstonewolfX 3095ecebf8
Update src/system/unlockables.ts
Fixing my silly typo

Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>
2024-08-27 14:09:26 -04:00
RedstonewolfX f5b49d812d 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

please forgive my jank
2024-08-27 10:56:26 -04:00
RedstonewolfX e9b06bdf1b Try (and fail) to write tests
Wrote a check (that might not even work) to make sure the player starts with a Map in the Daily Run

Attempted to write a check to see if Eviolite spawns in Daily Run, but can't figure out how to look for the right thing in the loot table

Attempted to write a check for shiny luck, but don't know how to manually set luck
2024-08-26 14:40:53 -04:00
RedstonewolfX 6225a020e0 Remove unused import 2024-08-26 13:13:29 -04:00
RedstonewolfX 48fbc571cd Update modifier-type.ts 2024-08-26 13:09:57 -04:00
RedstonewolfX 9baeac8d40
Update src/modifier/modifier-type.ts
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
2024-08-26 12:05:44 -04:00
RedstonewolfX 1b558cd945
Review Suggestions: Reformatting
Small edits to formatting as requested by flx

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
2024-08-26 12:04:53 -04:00
RedstonewolfX ae33a46945 Fix formatting
== my bebanished
2024-08-26 09:48:53 -04:00
RedstonewolfX e4c62e55cf Disable Eviolite in Daily Runs
Disables Eviolite spawning in Daily Run mode.
2024-08-26 01:52:19 -04:00
RedstonewolfX 590f1a6833 Give free map in daily
Adds a Map to the player's pool of starting items for Daily Runs.
2024-08-26 01:50:20 -04:00
RedstonewolfX 02e9273f97 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.
2024-08-26 01:47:07 -04:00
RedstonewolfX f8478aa79b
Merge branch 'pagefaultgames:beta' into beta 2024-08-26 01:39:10 -04:00
RedstonewolfX d1e14850ef
Fix parenthases order
oops

Co-authored-by: Leo Kim <47556641+KimJeongSun@users.noreply.github.com>
2024-08-21 16:36:43 -04:00
RedstonewolfX 103ad807ee
Fix potential divide by zero error
Thank you to KimJeongSun for bringing this up

Co-authored-by: Leo Kim <47556641+KimJeongSun@users.noreply.github.com>
2024-08-21 06:57:59 -04:00
RedstonewolfX 0be82b32dd Improve scroll bar
Remaking these changes on the beta branch since you're supposed to do it for the PR checklist
2024-08-18 12:52:06 -04:00
20 changed files with 430 additions and 99 deletions

View File

@ -1674,7 +1674,12 @@ export default class BattleScene extends SceneBase {
this.scoreText.setVisible(this.gameMode.isDaily); 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.
* @param isDaily If true, hides the label. (This is done because Luck does not apply in Daily Mode anymore)
*/
updateAndShowText(duration: number, isDaily?: boolean): void {
const labels = [ this.luckLabelText, this.luckText ]; const labels = [ this.luckLabelText, this.luckText ];
labels.forEach(t => t.setAlpha(0)); labels.forEach(t => t.setAlpha(0));
const luckValue = getPartyLuckValue(this.getParty()); const luckValue = getPartyLuckValue(this.getParty());
@ -1685,12 +1690,16 @@ export default class BattleScene extends SceneBase {
this.luckText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); this.luckText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969);
} }
this.luckLabelText.setX((this.game.canvas.width / 6) - 2 - (this.luckText.displayWidth + 2)); this.luckLabelText.setX((this.game.canvas.width / 6) - 2 - (this.luckText.displayWidth + 2));
if (isDaily) {
// Hide luck label
labels.forEach(t => t.setVisible(false));
}
this.tweens.add({ this.tweens.add({
targets: labels, targets: labels,
duration: duration, duration: duration,
alpha: 1, alpha: 1,
onComplete: () => { onComplete: () => {
labels.forEach(t => t.setVisible(true)); labels.forEach(t => t.setVisible(!isDaily));
} }
}); });
} }

View File

@ -1699,7 +1699,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.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.AMULET_COIN, skipInLastClassicWaveOrDefault(3)),
new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => { 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)) ? 10 : 0; 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)) ? 10 : 0;
} }
return 0; return 0;
@ -1777,7 +1778,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.MULTI_LENS, 18), new WeightedModifierType(modifierTypes.MULTI_LENS, 18),
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.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.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 => { ].map(m => {
m.setTier(ModifierTier.MASTER); return m; m.setTier(ModifierTier.MASTER); return m;
}) })
@ -1969,9 +1970,13 @@ export function getModifierPoolForType(poolType: ModifierPoolType): ModifierPool
} }
const tierWeights = [ 768 / 1024, 195 / 1024, 48 / 1024, 12 / 1024, 1 / 1024 ]; const tierWeights = [ 768 / 1024, 195 / 1024, 48 / 1024, 12 / 1024, 1 / 1024 ];
export const itemPoolChecks: Map<ModifierTypeKeys, boolean> = new Map();
export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: ModifierPoolType, rerollCount: integer = 0) { export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: ModifierPoolType, rerollCount: integer = 0) {
const pool = getModifierPoolForType(poolType); const pool = getModifierPoolForType(poolType);
itemPoolChecks.forEach((v, k) => {
itemPoolChecks.set(k, false);
});
const ignoredIndexes = {}; const ignoredIndexes = {};
const modifierTableData = {}; const modifierTableData = {};
@ -2008,6 +2013,9 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod
ignoredIndexes[t].push(i++); ignoredIndexes[t].push(i++);
return total; return total;
} }
if (itemPoolChecks.has(modifierType.modifierType.id as ModifierTypeKeys)) {
itemPoolChecks.set(modifierType.modifierType.id as ModifierTypeKeys, true);
}
thresholds.set(total, i++); thresholds.set(total, i++);
return total; return total;
}, 0); }, 0);
@ -2410,8 +2418,16 @@ export class ModifierTypeOption {
} }
} }
/**
* Calculates the team's luck value.
* @param party The player's party.
* @returns A value between 0 and 14, or 0 if the player is in Daily Run mode.
*/
export function getPartyLuckValue(party: Pokemon[]): integer { export function getPartyLuckValue(party: Pokemon[]): integer {
const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() : 0) if (party[0].scene.gameMode.isDaily) {
return 0;
}
const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? (p.scene.gameMode.isDaily ? 0 : p.getLuck()) : 0)
.reduce((total: integer, value: integer) => total += value, 0), 0, 14); .reduce((total: integer, value: integer) => total += value, 0), 0, 14);
return luck || 0; return luck || 0;
} }

View File

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

View File

@ -76,7 +76,8 @@ export class TitlePhase extends Phase {
this.scene.ui.clearText(); this.scene.ui.clearText();
this.end(); this.end();
}; };
if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { const { gameData } = this.scene;
if (gameData.isUnlocked(Unlockables.ENDLESS_MODE)) {
const options: OptionSelectItem[] = [ const options: OptionSelectItem[] = [
{ {
label: GameMode.getModeName(GameModes.CLASSIC), 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({ options.push({
label: GameMode.getModeName(GameModes.SPLICED_ENDLESS), label: GameMode.getModeName(GameModes.SPLICED_ENDLESS),
handler: () => { 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()) 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(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)) .concat(getDailyRunStarterModifiers(party))
.filter((m) => m !== null); .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> { public saveSystem(): Promise<boolean> {
return new Promise<boolean>(resolve => { return new Promise<boolean>(resolve => {
this.scene.ui.savingIcon.show(); this.scene.ui.savingIcon.show();

View File

@ -22,15 +22,25 @@ export function applySessionDataPatches(data: SessionSaveData) {
} else if (m.className === "PokemonResetNegativeStatStageModifier") { } else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier"; m.className = "ResetNegativeStatStageModifier";
} else if (m.className === "TempBattleStatBoosterModifier") { } else if (m.className === "TempBattleStatBoosterModifier") {
m.className = "TempStatStageBoosterModifier"; // Dire Hit no longer a part of the TempBattleStatBoosterModifierTypeGenerator
m.typeId = "TEMP_STAT_STAGE_BOOSTER"; if (m.typeId !== "DIRE_HIT") {
m.className = "TempStatStageBoosterModifier";
m.typeId = "TEMP_STAT_STAGE_BOOSTER";
// Migration from TempBattleStat to Stat // Migration from TempBattleStat to Stat
const newStat = m.typePregenArgs[0] + 1; const newStat = m.typePregenArgs[0] + 1;
m.typePregenArgs[0] = newStat; m.typePregenArgs[0] = newStat;
// From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ]
m.args = [ newStat, 5, m.args[1] ];
} else {
m.className = "TempCritBoosterModifier";
m.typePregenArgs = [];
// From [ stat, battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ 5, m.args[1] ];
}
// From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ]
m.args = [ newStat, 5, m.args[1] ];
} else if (m.className === "DoubleBattleChanceBoosterModifier" && m.args.length === 1) { } else if (m.className === "DoubleBattleChanceBoosterModifier" && m.args.length === 1) {
let maxBattles: number; let maxBattles: number;
switch (m.typeId) { switch (m.typeId) {
@ -73,7 +83,7 @@ export function applySystemDataPatches(data: SystemSaveData) {
case "1.0.3": case "1.0.3":
case "1.0.4": case "1.0.4":
// --- LEGACY PATCHES --- // --- LEGACY PATCHES ---
if (data.starterData) { if (data.starterData && data.dexData) {
// Migrate ability starter data if empty for caught species // Migrate ability starter data if empty for caught species
Object.keys(data.starterData).forEach(sd => { Object.keys(data.starterData).forEach(sd => {
if (data.dexData[sd]?.caughtAttr && (data.starterData[sd] && !data.starterData[sd].abilityAttr)) { if (data.dexData[sd]?.caughtAttr && (data.starterData[sd] && !data.starterData[sd].abilityAttr)) {
@ -104,12 +114,14 @@ export function applySystemDataPatches(data: SystemSaveData) {
// --- PATCHES --- // --- PATCHES ---
// Fix Starter Data // Fix Starter Data
for (const starterId of defaultStarterSpecies) { if (data.starterData && data.dexData) {
if (data.starterData[starterId]?.abilityAttr) { for (const starterId of defaultStarterSpecies) {
data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1; if (data.starterData[starterId]?.abilityAttr) {
} data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
if (data.dexData[starterId]?.caughtAttr) { }
data.dexData[starterId].caughtAttr |= DexAttr.FEMALE; if (data.dexData[starterId]?.caughtAttr) {
data.dexData[starterId].caughtAttr |= DexAttr.FEMALE;
}
} }
} }
} }

View File

@ -1,5 +1,15 @@
import { MapModifier } from "#app/modifier/modifier";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import GameManager from "./utils/gameManager"; import GameManager from "./utils/gameManager";
import { Moves } from "#app/enums/moves";
import { getPartyLuckValue, itemPoolChecks } from "#app/modifier/modifier-type";
import { Biome } from "#app/enums/biome";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import Overrides from "#app/overrides";
//const TIMEOUT = 20 * 1000;
describe("Daily Mode", () => { describe("Daily Mode", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -28,5 +38,97 @@ describe("Daily Mode", () => {
expect(pkm.level).toBe(20); expect(pkm.level).toBe(20);
expect(pkm.moveset.length).toBeGreaterThan(0); expect(pkm.moveset.length).toBeGreaterThan(0);
}); });
expect(game.scene.getModifiers(MapModifier).length).toBeGreaterThan(0);
}); });
}); });
//*
// Need to figure out how to properly start a battle
// Need to fix eviolite - test keeps insisting it is not in loot table, even though Mini Black Hole (which is using the exact same condition) is
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")
.shinyLevel(true)
.startingLevel(100) // Avoid levelling up
.enemyLevel(1000) // Avoid opponent dying before game.doKillOpponents()
.disableTrainerWaves()
.moveset([Moves.KOWTOW_CLEAVE])
.enemyMoveset(Moves.SPLASH);
itemPoolChecks.set("EVIOLITE", false);
itemPoolChecks.set("MINI_BLACK_HOLE", false);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
itemPoolChecks.clear();
});
it("should not have Eviolite and Mini Black Hole available in Classic if not unlocked", async () => {
await game.classicMode.runToSummon();
expect(itemPoolChecks.get("EVIOLITE")).toBeDefined();
expect(itemPoolChecks.get("EVIOLITE")).toBeFalsy();
expect(itemPoolChecks.get("MINI_BLACK_HOLE")).toBeDefined();
expect(itemPoolChecks.get("MINI_BLACK_HOLE")).toBeFalsy();
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);
expect(itemPoolChecks.get("EVIOLITE")).toBeDefined();
expect(itemPoolChecks.get("EVIOLITE")).toBeFalsy();
expect(itemPoolChecks.get("MINI_BLACK_HOLE")).toBeDefined();
expect(itemPoolChecks.get("MINI_BLACK_HOLE")).toBeFalsy();
});
});
it("should have Eviolite and Mini Black Hole available in Daily", async () => {
await game.dailyMode.runToSummon();
expect(itemPoolChecks.get("EVIOLITE")).toBeDefined();
expect(itemPoolChecks.get("EVIOLITE")).toBeFalsy();
expect(itemPoolChecks.get("MINI_BLACK_HOLE")).toBeDefined();
expect(itemPoolChecks.get("MINI_BLACK_HOLE")).toBeFalsy();
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);
expect(itemPoolChecks.get("EVIOLITE")).toBeDefined();
expect(itemPoolChecks.get("EVIOLITE")).toBeTruthy();
expect(itemPoolChecks.get("MINI_BLACK_HOLE")).toBeDefined();
expect(itemPoolChecks.get("MINI_BLACK_HOLE")).toBeTruthy();
});
});
it("should apply luck in Classic Mode", async () => {
await game.classicMode.runToSummon();
const party = game.scene.getParty();
expect(Overrides.SHINY_OVERRIDE).toBeTruthy();
expect(party[0]).toBeDefined();
expect(party[0].getLuck()).toBeGreaterThan(0);
expect(getPartyLuckValue(party)).toBeGreaterThan(0);
});
it("should not apply luck in Daily Run", async () => {
await game.dailyMode.runToSummon();
const party = game.scene.getParty();
expect(party[0]).toBeDefined();
expect(party[0].getLuck()).toBeGreaterThan(0);
expect(getPartyLuckValue(party)).toBe(0);
});
});
//*/

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

View File

@ -1,9 +1,13 @@
import { Species } from "#app/enums/species";
import { GameModes } from "#app/game-mode"; 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 GameManager from "#test/utils/gameManager";
import { MockClock } from "#test/utils/mocks/mockClock";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { Moves } from "#app/enums/moves";
import { Biome } from "#app/enums/biome";
describe("Reload", () => { describe("Reload", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -50,6 +54,13 @@ describe("Reload", () => {
game.move.select(Moves.KOWTOW_CLEAVE); game.move.select(Moves.KOWTOW_CLEAVE);
await game.phaseInterceptor.to("DamagePhase"); await game.phaseInterceptor.to("DamagePhase");
await game.doKillOpponents(); 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(); await game.toNextWave();
expect(game.phaseInterceptor.log).toContain("NewBiomeEncounterPhase"); expect(game.phaseInterceptor.log).toContain("NewBiomeEncounterPhase");

View File

@ -2,6 +2,7 @@ import { BerryType } from "#app/enums/berry-type";
import { Button } from "#app/enums/buttons"; import { Button } from "#app/enums/buttons";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species"; import { Species } from "#app/enums/species";
import { itemPoolChecks } from "#app/modifier/modifier-type";
import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
@ -94,3 +95,47 @@ describe("UI - Transfer Items", () => {
await game.phaseInterceptor.to(SelectModifierPhase); await game.phaseInterceptor.to(SelectModifierPhase);
}, 20000); }, 20000);
}); });
describe.skip("Backup Test", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(async () => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.startingLevel(100)
.startingWave(1)
.startingHeldItems([
{ name: "BERRY", count: 1, type: BerryType.SITRUS },
{ name: "BERRY", count: 2, type: BerryType.APICOT },
{ name: "BERRY", count: 2, type: BerryType.LUM },
])
.moveset([Moves.DRAGON_CLAW])
.enemySpecies(Species.MAGIKARP)
.enemyMoveset([Moves.SPLASH]);
itemPoolChecks.set("EVIOLITE", false);
itemPoolChecks.set("MINI_BLACK_HOLE", false);
});
it("should run", async () => {
await game.classicMode.startBattle([Species.RAYQUAZA, Species.RAYQUAZA, Species.RAYQUAZA]);
game.move.select(Moves.DRAGON_CLAW);
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
expect(game.scene.ui.getHandler()).toBeInstanceOf(ModifierSelectUiHandler);
});
await game.phaseInterceptor.to(BattleEndPhase);
});
});

View File

@ -10,6 +10,8 @@ import { ModifierOverride } from "#app/modifier/modifier-type";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { vi } from "vitest"; import { vi } from "vitest";
import { GameManagerHelper } from "./gameManagerHelper"; import { GameManagerHelper } from "./gameManagerHelper";
import { Unlockables } from "#app/system/unlockables";
import { Variant } from "#app/data/variant";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -300,6 +302,17 @@ export class OverridesHelper extends GameManagerHelper {
return this; return this;
} }
/**
* Gives the player access to an Unlockable.
* @param unlockable The Unlockable 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 * Override the items rolled at the end of a battle
* @param items the items to be rolled * @param items the items to be rolled
@ -311,6 +324,25 @@ export class OverridesHelper extends GameManagerHelper {
return this; 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 * Override the enemy (Pokemon) to have the given amount of health segments
* @param healthSegments the number of segments to give * @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 { VictoryPhase } from "#app/phases/victory-phase";
import { PartyHealPhase } from "#app/phases/party-heal-phase"; import { PartyHealPhase } from "#app/phases/party-heal-phase";
import UI, { Mode } from "#app/ui/ui"; import UI, { Mode } from "#app/ui/ui";
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
import { import {
MysteryEncounterBattlePhase, MysteryEncounterBattlePhase,
MysteryEncounterOptionSelectedPhase, MysteryEncounterOptionSelectedPhase,
@ -122,6 +123,7 @@ export default class PhaseInterceptor {
[EndEvolutionPhase, this.startPhase], [EndEvolutionPhase, this.startPhase],
[LevelCapPhase, this.startPhase], [LevelCapPhase, this.startPhase],
[AttemptRunPhase, this.startPhase], [AttemptRunPhase, this.startPhase],
[SelectBiomePhase, this.startPhase],
[MysteryEncounterPhase, this.startPhase], [MysteryEncounterPhase, this.startPhase],
[MysteryEncounterOptionSelectedPhase, this.startPhase], [MysteryEncounterOptionSelectedPhase, this.startPhase],
[MysteryEncounterBattlePhase, this.startPhase], [MysteryEncounterBattlePhase, this.startPhase],
@ -346,7 +348,8 @@ export default class PhaseInterceptor {
console.log("setMode", `${Mode[mode]} (=${mode})`, args); console.log("setMode", `${Mode[mode]} (=${mode})`, args);
const ret = this.originalSetMode.apply(instance, [mode, ...args]); const ret = this.originalSetMode.apply(instance, [mode, ...args]);
if (!this.phases[currentPhase.constructor.name]) { 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) { if (this.phases[currentPhase.constructor.name].endBySetMode) {
this.inProgress?.callback(); 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 * @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> { 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; return false;
} }
if (scene.gameData.getTutorialFlags()[tutorial] && !Overrides.BYPASS_TUTORIAL_SKIP) { if (scene.gameData.getTutorialFlags()[tutorial] && !Overrides.BYPASS_TUTORIAL_SKIP_OVERRIDE) {
return false; return false;
} }

View File

@ -344,6 +344,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
super.clear(); super.clear();
this.config = null; this.config = null;
this.optionSelectContainer.setVisible(false); this.optionSelectContainer.setVisible(false);
this.scrollCursor = 0;
this.eraseCursor(); this.eraseCursor();
} }

View File

@ -1,12 +1,13 @@
import BattleScene from "../battle-scene"; import BattleScene from "#app/battle-scene";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import i18next from "i18next"; import i18next from "i18next";
import { Achv, achvs, getAchievementDescription } from "../system/achv"; import { Achv, achvs, getAchievementDescription } from "#app/system/achv";
import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "../system/voucher"; import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#app/system/voucher";
import MessageUiHandler from "./message-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler";
import { addTextObject, TextStyle } from "./text"; import { addTextObject, TextStyle } from "#app/ui/text";
import { Mode } from "./ui"; import { Mode } from "#app/ui/ui";
import { addWindow } from "./ui-theme"; import { addWindow } from "#app/ui/ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
enum Page { enum Page {
@ -49,6 +50,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
private vouchersTotal: number; private vouchersTotal: number;
private currentTotal: number; private currentTotal: number;
private scrollBar: ScrollBar;
private scrollCursor: number; private scrollCursor: number;
private cursorObj: Phaser.GameObjects.NineSlice | null; private cursorObj: Phaser.GameObjects.NineSlice | null;
private currentPage: Page; private currentPage: Page;
@ -91,7 +93,10 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.iconsBg = addWindow(this.scene, 0, this.headerBg.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - this.headerBg.height - 68); this.iconsBg = addWindow(this.scene, 0, this.headerBg.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - this.headerBg.height - 68);
this.iconsBg.setOrigin(0, 0); this.iconsBg.setOrigin(0, 0);
this.iconsContainer = this.scene.add.container(6, this.headerBg.height + 6); const yOffset = 6;
this.scrollBar = new ScrollBar(this.scene, this.iconsBg.width - 9, this.iconsBg.y + yOffset, 4, this.iconsBg.height - yOffset * 2, this.ROWS);
this.iconsContainer = this.scene.add.container(5, this.headerBg.height + 8);
this.icons = []; this.icons = [];
@ -148,6 +153,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.mainContainer.add(this.headerText); this.mainContainer.add(this.headerText);
this.mainContainer.add(this.headerActionText); this.mainContainer.add(this.headerActionText);
this.mainContainer.add(this.iconsBg); this.mainContainer.add(this.iconsBg);
this.mainContainer.add(this.scrollBar);
this.mainContainer.add(this.iconsContainer); this.mainContainer.add(this.iconsContainer);
this.mainContainer.add(titleBg); this.mainContainer.add(titleBg);
this.mainContainer.add(this.titleText); this.mainContainer.add(this.titleText);
@ -162,6 +168,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.currentPage = Page.ACHIEVEMENTS; this.currentPage = Page.ACHIEVEMENTS;
this.setCursor(0); this.setCursor(0);
this.setScrollCursor(0);
this.mainContainer.setVisible(false); this.mainContainer.setVisible(false);
} }
@ -175,6 +182,8 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.mainContainer.setVisible(true); this.mainContainer.setVisible(true);
this.setCursor(0); this.setCursor(0);
this.setScrollCursor(0); this.setScrollCursor(0);
this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS));
this.scrollBar.setScrollCursor(0);
this.getUi().moveTo(this.mainContainer, this.getUi().length - 1); this.getUi().moveTo(this.mainContainer, this.getUi().length - 1);
@ -224,6 +233,8 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.updateAchvIcons(); this.updateAchvIcons();
} }
this.setCursor(0, true); this.setCursor(0, true);
this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS));
this.scrollBar.setScrollCursor(0);
this.mainContainer.update(); this.mainContainer.update();
} }
if (button === Button.CANCEL) { if (button === Button.CANCEL) {
@ -237,32 +248,44 @@ export default class AchvsUiHandler extends MessageUiHandler {
if (this.cursor < this.COLS) { if (this.cursor < this.COLS) {
if (this.scrollCursor) { if (this.scrollCursor) {
success = this.setScrollCursor(this.scrollCursor - 1); success = this.setScrollCursor(this.scrollCursor - 1);
} else {
// Wrap around to the last row
success = this.setScrollCursor(Math.ceil(this.currentTotal / this.COLS) - this.ROWS);
let newCursorIndex = this.cursor + (this.ROWS - 1) * this.COLS;
if (newCursorIndex > this.currentTotal - this.scrollCursor * this.COLS -1) {
newCursorIndex -= this.COLS;
}
success = success && this.setCursor(newCursorIndex);
} }
} else { } else {
success = this.setCursor(this.cursor - this.COLS); success = this.setCursor(this.cursor - this.COLS);
} }
break; break;
case Button.DOWN: case Button.DOWN:
const canMoveDown = (this.cursor + itemOffset) + this.COLS < this.currentTotal; const canMoveDown = itemOffset + 1 < this.currentTotal;
if (rowIndex >= this.ROWS - 1) { if (rowIndex >= this.ROWS - 1) {
if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) { if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) {
// scroll down one row
success = this.setScrollCursor(this.scrollCursor + 1); success = this.setScrollCursor(this.scrollCursor + 1);
} else {
// wrap back to the first row
success = this.setScrollCursor(0) && this.setCursor(this.cursor % this.COLS);
} }
} else if (canMoveDown) { } else if (canMoveDown) {
success = this.setCursor(this.cursor + this.COLS); success = this.setCursor(Math.min(this.cursor + this.COLS, this.currentTotal - itemOffset - 1));
} }
break; break;
case Button.LEFT: case Button.LEFT:
if (!this.cursor && this.scrollCursor) { if (this.cursor % this.COLS === 0) {
success = this.setScrollCursor(this.scrollCursor - 1) && this.setCursor(this.cursor + (this.COLS - 1)); success = this.setCursor(Math.min(this.cursor + this.COLS - 1, this.currentTotal - itemOffset - 1));
} else if (this.cursor) { } else {
success = this.setCursor(this.cursor - 1); success = this.setCursor(this.cursor - 1);
} }
break; break;
case Button.RIGHT: case Button.RIGHT:
if (this.cursor + 1 === this.ROWS * this.COLS && this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS) { if ((this.cursor + 1) % this.COLS === 0 || (this.cursor + itemOffset) === (this.currentTotal - 1)) {
success = this.setScrollCursor(this.scrollCursor + 1) && this.setCursor(this.cursor - (this.COLS - 1)); success = this.setCursor(this.cursor - this.cursor % this.COLS);
} else if (this.cursor + itemOffset < this.currentTotal - 1) { } else {
success = this.setCursor(this.cursor + 1); success = this.setCursor(this.cursor + 1);
} }
break; break;
@ -315,15 +338,22 @@ export default class AchvsUiHandler extends MessageUiHandler {
} }
this.scrollCursor = scrollCursor; this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
// Cursor cannot go farther than the last element in the list
const maxCursor = Math.min(this.cursor, this.currentTotal - this.scrollCursor * this.COLS - 1);
if (maxCursor !== this.cursor) {
this.setCursor(maxCursor);
}
switch (this.currentPage) { switch (this.currentPage) {
case Page.ACHIEVEMENTS: case Page.ACHIEVEMENTS:
this.updateAchvIcons(); this.updateAchvIcons();
this.showAchv(achvs[Object.keys(achvs)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(achvs).length - 1)]]); this.showAchv(achvs[Object.keys(achvs)[this.cursor + this.scrollCursor * this.COLS]]);
break; break;
case Page.VOUCHERS: case Page.VOUCHERS:
this.updateVoucherIcons(); this.updateVoucherIcons();
this.showVoucher(vouchers[Object.keys(vouchers)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(vouchers).length - 1)]]); this.showVoucher(vouchers[Object.keys(vouchers)[this.cursor + this.scrollCursor * this.COLS]]);
break; break;
} }
return true; return true;
@ -411,6 +441,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
super.clear(); super.clear();
this.currentPage = Page.ACHIEVEMENTS; this.currentPage = Page.ACHIEVEMENTS;
this.mainContainer.setVisible(false); this.mainContainer.setVisible(false);
this.setScrollCursor(0);
this.eraseCursor(); this.eraseCursor();
} }

View File

@ -230,7 +230,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
/* Multiplies the appearance duration by the speed parameter so that it is always constant, and avoids "flashbangs" at game speed x5 */ /* Multiplies the appearance duration by the speed parameter so that it is always constant, and avoids "flashbangs" at game speed x5 */
this.scene.showShopOverlay(750 * this.scene.gameSpeed); this.scene.showShopOverlay(750 * this.scene.gameSpeed);
this.scene.updateAndShowText(750); this.scene.updateAndShowText(750, this.scene.gameMode.isDaily);
this.scene.updateBiomeWaveText(); this.scene.updateBiomeWaveText();
this.scene.updateMoneyText(); this.scene.updateMoneyText();

View File

@ -1,36 +1,65 @@
/**
* A vertical scrollbar element that resizes dynamically based on the current scrolling
* and number of elements that can be shown on screen
*/
export class ScrollBar extends Phaser.GameObjects.Container { export class ScrollBar extends Phaser.GameObjects.Container {
private bg: Phaser.GameObjects.Image; private bg: Phaser.GameObjects.NineSlice;
private handleBody: Phaser.GameObjects.Rectangle; private handleBody: Phaser.GameObjects.Rectangle;
private handleBottom: Phaser.GameObjects.Image; private handleBottom: Phaser.GameObjects.NineSlice;
private pages: number; private currentRow: number;
private page: number; private totalRows: number;
private maxRows: number;
constructor(scene: Phaser.Scene, x: number, y: number, pages: number) { /**
* @param scene the current scene
* @param x the scrollbar's x position (origin: top left)
* @param y the scrollbar's y position (origin: top left)
* @param width the scrollbar's width
* @param height the scrollbar's height
* @param maxRows the maximum number of rows that can be shown at once
*/
constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number, maxRows: number) {
super(scene, x, y); super(scene, x, y);
this.bg = scene.add.image(0, 0, "scroll_bar"); this.maxRows = maxRows;
const borderSize = 2;
width = Math.max(width, 4);
this.bg = scene.add.nineslice(0, 0, "scroll_bar", undefined, width, height, borderSize, borderSize, borderSize, borderSize);
this.bg.setOrigin(0, 0); this.bg.setOrigin(0, 0);
this.add(this.bg); this.add(this.bg);
this.handleBody = scene.add.rectangle(1, 1, 3, 4, 0xaaaaaa); this.handleBody = scene.add.rectangle(1, 1, width - 2, 4, 0xaaaaaa);
this.handleBody.setOrigin(0, 0); this.handleBody.setOrigin(0, 0);
this.add(this.handleBody); this.add(this.handleBody);
this.handleBottom = scene.add.image(1, 1, "scroll_bar_handle"); this.handleBottom = scene.add.nineslice(1, 1, "scroll_bar_handle", undefined, width - 2, 2, 2, 0, 0, 0);
this.handleBottom.setOrigin(0, 0); this.handleBottom.setOrigin(0, 0);
this.add(this.handleBottom); this.add(this.handleBottom);
} }
setPage(page: number): void { /**
this.page = page; * Set the current row that is displayed
this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.pages * page; * Moves the bar handle up or down accordingly
* @param scrollCursor how many times the view was scrolled down
*/
setScrollCursor(scrollCursor: number): void {
this.currentRow = scrollCursor;
this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.totalRows * this.currentRow;
this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight; this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight;
} }
setPages(pages: number): void { /**
this.pages = pages; * Set the total number of rows to display
this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * 9 / this.pages; * If it's smaller than the maximum number of rows on screen the bar will get hidden
* Otherwise the scrollbar handle gets resized based on the ratio to the maximum number of rows
* @param rows how many rows of data there are in total
*/
setTotalRows(rows: number): void {
this.totalRows = rows;
this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * this.maxRows / this.totalRows;
this.setVisible(this.pages > 9); this.setVisible(this.totalRows > this.maxRows);
} }
} }

View File

@ -1,11 +1,12 @@
import UiHandler from "../ui-handler"; import UiHandler from "#app/ui/ui-handler";
import BattleScene from "../../battle-scene"; import BattleScene from "#app/battle-scene";
import {Mode} from "../ui"; import { Mode } from "#app/ui/ui";
import {InterfaceConfig} from "../../inputs-controller"; import { InterfaceConfig } from "#app/inputs-controller";
import {addWindow} from "../ui-theme"; import { addWindow } from "#app/ui/ui-theme";
import {addTextObject, TextStyle} from "../text"; import { addTextObject, TextStyle } from "#app/ui/text";
import {getIconWithSettingName} from "#app/configs/inputs/configHandler"; import { ScrollBar } from "#app/ui/scroll-bar";
import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; import { getIconWithSettingName } from "#app/configs/inputs/configHandler";
import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu";
import { Device } from "#enums/devices"; import { Device } from "#enums/devices";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import i18next from "i18next"; import i18next from "i18next";
@ -19,7 +20,7 @@ export interface LayoutConfig {
inputsIcons: InputsIcons; inputsIcons: InputsIcons;
settingLabels: Phaser.GameObjects.Text[]; settingLabels: Phaser.GameObjects.Text[];
optionValueLabels: Phaser.GameObjects.Text[][]; optionValueLabels: Phaser.GameObjects.Text[][];
optionCursors: integer[]; optionCursors: number[];
keys: string[]; keys: string[];
bindingSettings: Array<String>; bindingSettings: Array<String>;
} }
@ -31,8 +32,9 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
protected optionsContainer: Phaser.GameObjects.Container; protected optionsContainer: Phaser.GameObjects.Container;
protected navigationContainer: NavigationMenu; protected navigationContainer: NavigationMenu;
protected scrollCursor: integer; protected scrollBar: ScrollBar;
protected optionCursors: integer[]; protected scrollCursor: number;
protected optionCursors: number[];
protected cursorObj: Phaser.GameObjects.NineSlice | null; protected cursorObj: Phaser.GameObjects.NineSlice | null;
protected optionsBg: Phaser.GameObjects.NineSlice; protected optionsBg: Phaser.GameObjects.NineSlice;
@ -65,7 +67,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
protected device: Device; protected device: Device;
abstract saveSettingToLocalStorage(setting, cursor): void; abstract saveSettingToLocalStorage(setting, cursor): void;
abstract setSetting(scene: BattleScene, setting, value: integer): boolean; abstract setSetting(scene: BattleScene, setting, value: number): boolean;
/** /**
* Constructor for the AbstractSettingsUiHandler. * Constructor for the AbstractSettingsUiHandler.
@ -241,7 +243,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Calculate the total available space for placing option labels next to their setting label // Calculate the total available space for placing option labels next to their setting label
// We reserve space for the setting label and then distribute the remaining space evenly // We reserve space for the setting label and then distribute the remaining space evenly
const totalSpace = (300 - labelWidth) - totalWidth / 6; const totalSpace = (297 - labelWidth) - totalWidth / 6;
// Calculate the spacing between options based on the available space divided by the number of gaps between labels // Calculate the spacing between options based on the available space divided by the number of gaps between labels
const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1));
@ -269,6 +271,11 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Add the options container to the overall settings container to be displayed in the UI. // Add the options container to the overall settings container to be displayed in the UI.
this.settingsContainer.add(optionsContainer); this.settingsContainer.add(optionsContainer);
} }
// Add vertical scrollbar
this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay);
this.settingsContainer.add(this.scrollBar);
// Add the settings container to the UI. // Add the settings container to the UI.
ui.add(this.settingsContainer); ui.add(this.settingsContainer);
@ -413,6 +420,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
this.optionCursors = layout.optionCursors; this.optionCursors = layout.optionCursors;
this.inputsIcons = layout.inputsIcons; this.inputsIcons = layout.inputsIcons;
this.bindingSettings = layout.bindingSettings; this.bindingSettings = layout.bindingSettings;
this.scrollBar.setTotalRows(layout.settingLabels.length);
this.scrollBar.setScrollCursor(0);
// Return true indicating the layout was successfully applied. // Return true indicating the layout was successfully applied.
return true; return true;
@ -538,7 +547,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param cursor - The cursor position to set. * @param cursor - The cursor position to set.
* @returns `true` if the cursor was set successfully. * @returns `true` if the cursor was set successfully.
*/ */
setCursor(cursor: integer): boolean { setCursor(cursor: number): boolean {
const ret = super.setCursor(cursor); const ret = super.setCursor(cursor);
// If the optionsContainer is not initialized, return the result from the parent class directly. // If the optionsContainer is not initialized, return the result from the parent class directly.
if (!this.optionsContainer) { if (!this.optionsContainer) {
@ -547,7 +556,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Check if the cursor object exists, if not, create it. // Check if the cursor object exists, if not, create it.
if (!this.cursorObj) { if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10);
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1);
this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner. this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner.
this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container. this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container.
} }
@ -564,7 +574,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param scrollCursor - The scroll cursor position to set. * @param scrollCursor - The scroll cursor position to set.
* @returns `true` if the scroll cursor was set successfully. * @returns `true` if the scroll cursor was set successfully.
*/ */
setScrollCursor(scrollCursor: integer): boolean { setScrollCursor(scrollCursor: number): boolean {
// Check if the new scroll position is the same as the current one; if so, do not update. // Check if the new scroll position is the same as the current one; if so, do not update.
if (scrollCursor === this.scrollCursor) { if (scrollCursor === this.scrollCursor) {
return false; return false;
@ -572,6 +582,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Update the internal scroll cursor state // Update the internal scroll cursor state
this.scrollCursor = scrollCursor; this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
// Apply the new scroll position to the settings UI. // Apply the new scroll position to the settings UI.
this.updateSettingsScroll(); this.updateSettingsScroll();
@ -590,7 +601,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param save - Whether to save the setting to local storage. * @param save - Whether to save the setting to local storage.
* @returns `true` if the option cursor was set successfully. * @returns `true` if the option cursor was set successfully.
*/ */
setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
// Retrieve the specific setting using the settingIndex from the settingDevice enumeration. // Retrieve the specific setting using the settingIndex from the settingDevice enumeration.
const setting = this.setting[Object.keys(this.setting)[settingIndex]]; const setting = this.setting[Object.keys(this.setting)[settingIndex]];

View File

@ -1,12 +1,13 @@
import BattleScene from "../../battle-scene"; import BattleScene from "#app/battle-scene";
import { hasTouchscreen, isMobile } from "../../touch-controls"; import { hasTouchscreen, isMobile } from "#app/touch-controls";
import { TextStyle, addTextObject } from "../text"; import { TextStyle, addTextObject } from "#app/ui/text";
import { Mode } from "../ui"; import { Mode } from "#app/ui/ui";
import UiHandler from "../ui-handler"; import UiHandler from "#app/ui/ui-handler";
import { addWindow } from "../ui-theme"; import { addWindow } from "#app/ui/ui-theme";
import {Button} from "#enums/buttons"; import { ScrollBar } from "#app/ui/scroll-bar";
import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler"; import { Button } from "#enums/buttons";
import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; import { InputsIcons } from "#app/ui/settings/abstract-control-settings-ui-handler";
import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu";
import { Setting, SettingKeys, SettingType } from "#app/system/settings/settings"; import { Setting, SettingKeys, SettingType } from "#app/system/settings/settings";
import i18next from "i18next"; import i18next from "i18next";
@ -19,11 +20,12 @@ export default class AbstractSettingsUiHandler extends UiHandler {
private optionsContainer: Phaser.GameObjects.Container; private optionsContainer: Phaser.GameObjects.Container;
private navigationContainer: NavigationMenu; private navigationContainer: NavigationMenu;
private scrollCursor: integer; private scrollCursor: number;
private scrollBar: ScrollBar;
private optionsBg: Phaser.GameObjects.NineSlice; private optionsBg: Phaser.GameObjects.NineSlice;
private optionCursors: integer[]; private optionCursors: number[];
private settingLabels: Phaser.GameObjects.Text[]; private settingLabels: Phaser.GameObjects.Text[];
private optionValueLabels: Phaser.GameObjects.Text[][]; private optionValueLabels: Phaser.GameObjects.Text[][];
@ -117,7 +119,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8); const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8);
const totalSpace = (300 - labelWidth) - totalWidth / 6; const totalSpace = (297 - labelWidth) - totalWidth / 6;
const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1)); const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1));
let xOffset = 0; let xOffset = 0;
@ -130,7 +132,11 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.optionCursors = this.settings.map(setting => setting.default); this.optionCursors = this.settings.map(setting => setting.default);
this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay);
this.scrollBar.setTotalRows(this.settings.length);
this.settingsContainer.add(this.optionsBg); this.settingsContainer.add(this.optionsBg);
this.settingsContainer.add(this.scrollBar);
this.settingsContainer.add(this.navigationContainer); this.settingsContainer.add(this.navigationContainer);
this.settingsContainer.add(actionsBg); this.settingsContainer.add(actionsBg);
this.settingsContainer.add(this.optionsContainer); this.settingsContainer.add(this.optionsContainer);
@ -186,6 +192,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.settingsContainer.setVisible(true); this.settingsContainer.setVisible(true);
this.setCursor(0); this.setCursor(0);
this.setScrollCursor(0);
this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1);
@ -301,11 +308,12 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param cursor - The cursor position to set. * @param cursor - The cursor position to set.
* @returns `true` if the cursor was set successfully. * @returns `true` if the cursor was set successfully.
*/ */
setCursor(cursor: integer): boolean { setCursor(cursor: number): boolean {
const ret = super.setCursor(cursor); const ret = super.setCursor(cursor);
if (!this.cursorObj) { if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10);
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1);
this.cursorObj.setOrigin(0, 0); this.cursorObj.setOrigin(0, 0);
this.optionsContainer.add(this.cursorObj); this.optionsContainer.add(this.cursorObj);
} }
@ -323,7 +331,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param save - Whether to save the setting to local storage. * @param save - Whether to save the setting to local storage.
* @returns `true` if the option cursor was set successfully. * @returns `true` if the option cursor was set successfully.
*/ */
setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
const setting = this.settings[settingIndex]; const setting = this.settings[settingIndex];
if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) { if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) {
@ -359,12 +367,13 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param scrollCursor - The scroll cursor position to set. * @param scrollCursor - The scroll cursor position to set.
* @returns `true` if the scroll cursor was set successfully. * @returns `true` if the scroll cursor was set successfully.
*/ */
setScrollCursor(scrollCursor: integer): boolean { setScrollCursor(scrollCursor: number): boolean {
if (scrollCursor === this.scrollCursor) { if (scrollCursor === this.scrollCursor) {
return false; return false;
} }
this.scrollCursor = scrollCursor; this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
this.updateSettingsScroll(); this.updateSettingsScroll();
@ -394,6 +403,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
clear() { clear() {
super.clear(); super.clear();
this.settingsContainer.setVisible(false); this.settingsContainer.setVisible(false);
this.setScrollCursor(0);
this.eraseCursor(); this.eraseCursor();
this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar); this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar);
if (this.reloadRequired) { if (this.reloadRequired) {

View File

@ -627,7 +627,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const starterBoxContainer = this.scene.add.container(speciesContainerX + 6, 9); //115 const starterBoxContainer = this.scene.add.container(speciesContainerX + 6, 9); //115
this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 0); this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 5, starterContainerWindow.height - 6, 9);
starterBoxContainer.add(this.starterSelectScrollBar); starterBoxContainer.add(this.starterSelectScrollBar);
@ -2540,8 +2540,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
} }
}); });
this.starterSelectScrollBar.setPages(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1));
this.starterSelectScrollBar.setPage(0); this.starterSelectScrollBar.setScrollCursor(0);
// sort // sort
const sort = this.filterBar.getVals(DropDownColumn.SORT)[0]; const sort = this.filterBar.getVals(DropDownColumn.SORT)[0];
@ -2576,7 +2576,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const onScreenFirstIndex = this.scrollCursor * maxColumns; const onScreenFirstIndex = this.scrollCursor * maxColumns;
const onScreenLastIndex = Math.min(this.filteredStarterContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns -1); const onScreenLastIndex = Math.min(this.filteredStarterContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns -1);
this.starterSelectScrollBar.setPage(this.scrollCursor); this.starterSelectScrollBar.setScrollCursor(this.scrollCursor);
let pokerusCursorIndex = 0; let pokerusCursorIndex = 0;
this.filteredStarterContainers.forEach((container, i) => { this.filteredStarterContainers.forEach((container, i) => {