Compare commits

...

19 Commits

Author SHA1 Message Date
Yiling Kang 2290afb013
Merge a7113065c0 into 51bb80cb66 2024-09-18 20:00:32 -07:00
flx-sta a7113065c0
Merge branch 'beta' into implSynchronize 2024-09-18 20:00:30 -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
innerthunder 605ae9e1c3
[Move] Improved damage forecasting for Shell Side Arm (#4310) 2024-09-18 19:03:01 -04:00
Madmadness65 81ea1296b3
[Miscellaneous] Add new Lake and RUins biome BGM by Lmz (#4319)
* Add new biome BGM by Firel

* Add new biome BGM by Lmz

* Update bgm-name.json
2024-09-18 19:00:06 -04:00
flx-sta d2e7a4d7bb
Merge branch 'beta' into implSynchronize 2024-09-17 20:21:01 -07:00
NightKev 3ef3f73ff7
Merge branch 'beta' into implSynchronize 2024-09-17 18:44:48 -07:00
NightKev 0157eba750
Merge branch 'beta' into implSynchronize 2024-09-11 07:23:39 -07:00
NightKev 580d03972e Add `simulated` support 2024-09-08 22:31:05 -07:00
NightKev a25de2dcdf
Merge branch 'beta' into implSynchronize 2024-09-08 20:57:10 -07:00
NightKev 5b53aff5b7 Remove impossible `if` statement 2024-09-08 19:16:32 -07:00
NightKev 84741bad45 Formatting change 2024-09-08 18:30:35 -07:00
NightKev 9ec78c47a7 Update tests 2024-09-08 18:27:19 -07:00
NightKev b11c17d1f6 Merge branch 'beta' into implSynchronize 2024-09-08 18:09:18 -07:00
Yiling Kang fd47aa8af4 Fix some spacing 2024-07-03 23:20:14 -07:00
Yiling Kang 256670e3b4 Resolve merge conflicts 2024-07-03 23:19:14 -07:00
Yiling Kang ec82686305 Update to show ability even if opponent pokemon does not get statused 2024-07-01 13:31:38 -07:00
Yiling Kang aadc86dd19 Fix psycho shift interaction causing buggy behaviour 2024-07-01 13:00:33 -07:00
Yiling Kang 7067187532 Initial changes for Synchronize ability 2024-07-01 02:29:39 -07:00
20 changed files with 498 additions and 132 deletions

View File

@ -56,7 +56,7 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to
- Pokémon Legends: Arceus
- Pokémon Scarlet/Violet
- Firel (Custom Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music)
- Lmz (Custom Jungle biome music)
- Lmz (Custom Ancient Ruins, Jungle, and Lake biome music)
- Andr06 (Custom Slum and Sea biome music)
### 🎵 Sound Effects

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

62
src/data/ability.ts Executable file → Normal file
View File

@ -1785,6 +1785,61 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
}
}
/**
* Base class for defining all {@linkcode Ability} Attributes after a status effect has been set.
* @see {@linkcode applyPostSetStatus()}.
*/
export class PostSetStatusAbAttr extends AbAttr {
/**
* Does nothing after a status condition is set.
* @param pokemon {@linkcode Pokemon} that status condition was set on.
* @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is `null` if status was not set by a Pokemon.
* @param passive Whether this ability is a passive.
* @param effect {@linkcode StatusEffect} that was set.
* @param args Set of unique arguments needed by this attribute.
* @returns `true` if application of the ability succeeds.
*/
applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, simulated: boolean, args: any[]) : boolean | Promise<boolean> {
return false;
}
}
/**
* If another Pokemon burns, paralyzes, poisons, or badly poisons this Pokemon,
* that Pokemon receives the same non-volatile status condition as part of this
* ability attribute. For Synchronize ability.
*/
export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr {
/**
* If the `StatusEffect` that was set is Burn, Paralysis, Poison, or Toxic, and the status
* was set by a source Pokemon, set the source Pokemon's status to the same `StatusEffect`.
* @param pokemon {@linkcode Pokemon} that status condition was set on.
* @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is null if status was not set by a Pokemon.
* @param passive Whether this ability is a passive.
* @param effect {@linkcode StatusEffect} that was set.
* @param args Set of unique arguments needed by this attribute.
* @returns `true` if application of the ability succeeds.
*/
override applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, simulated:boolean, args: any[]): boolean {
/** Synchronizable statuses */
const syncStatuses = new Set<StatusEffect>([
StatusEffect.BURN,
StatusEffect.PARALYSIS,
StatusEffect.POISON,
StatusEffect.TOXIC
]);
if (sourcePokemon && syncStatuses.has(effect)) {
if (!simulated) {
sourcePokemon.trySetStatus(effect, true, pokemon);
}
return true;
}
return false;
}
}
export class PostVictoryAbAttr extends AbAttr {
applyPostVictory(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
return false;
@ -4664,6 +4719,10 @@ export function applyStatMultiplierAbAttrs(attrType: Constructor<StatMultiplierA
pokemon: Pokemon, stat: BattleStat, statValue: Utils.NumberHolder, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<StatMultiplierAbAttr>(attrType, pokemon, (attr, passive) => attr.applyStatStage(pokemon, passive, simulated, stat, statValue, args), args);
}
export function applyPostSetStatusAbAttrs(attrType: Constructor<PostSetStatusAbAttr>,
pokemon: Pokemon, effect: StatusEffect, sourcePokemon?: Pokemon | null, simulated: boolean = false, ...args: any[]): Promise<void> {
return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args);
}
/**
* Applies a field Stat multiplier attribute
@ -4896,7 +4955,8 @@ export function initAbilities() {
.attr(EffectSporeAbAttr),
new Ability(Abilities.SYNCHRONIZE, 3)
.attr(SyncEncounterNatureAbAttr)
.unimplemented(),
.attr(SynchronizeStatusAbAttr)
.partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now
new Ability(Abilities.CLEAR_BODY, 3)
.attr(ProtectStatAbAttr)
.ignorable(),

View File

@ -2093,21 +2093,20 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
if (target.status) {
return false;
}
//@ts-ignore - how can target.status.effect be checked when we return `false` before when it's defined?
if (!target.status || (target.status.effect === statusToApply && move.chance < 0)) { // TODO: resolve ts-ignore
const statusAfflictResult = target.trySetStatus(statusToApply, true, user);
if (statusAfflictResult) {
} else {
const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
if (canSetStatus) {
if (user.status) {
user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user)));
}
user.resetStatus();
user.updateInfo();
}
return statusAfflictResult;
target.trySetStatus(statusToApply, true, user);
}
return false;
return canSetStatus;
}
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
@ -3974,18 +3973,17 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr {
export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.NumberHolder);
const atkRatio = user.getEffectiveStat(Stat.ATK, target, move) / target.getEffectiveStat(Stat.DEF, user, move);
const specialRatio = user.getEffectiveStat(Stat.SPATK, target, move) / target.getEffectiveStat(Stat.SPDEF, user, move);
// Shell Side Arm is much more complicated than it looks, this is a partial implementation to try to achieve something similar to the games
if (atkRatio > specialRatio) {
const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true);
const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true);
if (predictedPhysDmg > predictedSpecDmg) {
category.value = MoveCategory.PHYSICAL;
return true;
} else if (atkRatio === specialRatio && user.randSeedInt(2) === 0) {
} else if (predictedPhysDmg === predictedSpecDmg && user.randSeedInt(2) === 0) {
category.value = MoveCategory.PHYSICAL;
return true;
}
return false;
}
}
@ -9106,7 +9104,7 @@ export function initMoves() {
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
.attr(ShellSideArmCategoryAttr)
.attr(StatusEffectAttr, StatusEffect.POISON)
.partial(),
.partial(), // Physical version of the move does not make contact
new AttackMove(Moves.MISTY_EXPLOSION, Type.FAIRY, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8)
.attr(SacrificialAttr)
.target(MoveTarget.ALL_NEAR_OTHERS)

View File

@ -762,7 +762,7 @@ export class Arena {
case Biome.BEACH:
return 3.462;
case Biome.LAKE:
return 5.350;
return 7.215;
case Biome.SEABED:
return 2.600;
case Biome.MOUNTAIN:
@ -788,7 +788,7 @@ export class Arena {
case Biome.FACTORY:
return 4.985;
case Biome.RUINS:
return 2.270;
return 0.000;
case Biome.WASTELAND:
return 6.336;
case Biome.ABYSS:

View File

@ -20,7 +20,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "../data/ability";
import PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui";
@ -2322,11 +2322,61 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return accuracyMultiplier.value / evasionMultiplier.value;
}
/**
* Calculates the base damage of the given move against this Pokemon when attacked by the given source.
* Used during damage calculation and for Shell Side Arm's forecasting effect.
* @param source the attacking {@linkcode Pokemon}.
* @param move the {@linkcode Move} used in the attack.
* @param moveCategory the move's {@linkcode MoveCategory} after variable-category effects are applied.
* @param ignoreAbility if `true`, ignores this Pokemon's defensive ability effects (defaults to `false`).
* @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`).
* @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`).
* @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`).
* @returns The move's base damage against this Pokemon when used by the source Pokemon.
*/
getBaseDamage(source: Pokemon, move: Move, moveCategory: MoveCategory, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number {
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
/** A base damage multiplier based on the source's level */
const levelMultiplier = (2 * source.level / 5 + 2);
/** The power of the move after power boosts from abilities, etc. have applied */
const power = move.calculateBattlePower(source, this, simulated);
/**
* The attacker's offensive stat for the given move's category.
* Critical hits cause negative stat stages to be ignored.
*/
const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated));
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
/**
* This Pokemon's defensive stat for the given move's category.
* Critical hits cause positive stat stages to be ignored.
*/
const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated));
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
/**
* The attack's base damage, as determined by the source's level, move power
* and Attack stat as well as this Pokemon's Defense stat
*/
const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2;
/** Debug message for non-simulated calls (i.e. when damage is actually dealt) */
if (!simulated) {
console.log("base damage", baseDamage, move.name, power, sourceAtk.value, targetDef.value);
}
return baseDamage;
}
/**
* Calculates the damage of an attack made by another Pokemon against this Pokemon
* @param source {@linkcode Pokemon} the attacking Pokemon
* @param move {@linkcode Pokemon} the move used in the attack
* @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects
* @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects
* @param isCritical If `true`, calculates damage for a critical hit.
* @param simulated If `true`, suppresses changes to game state during the calculation.
* @returns a {@linkcode DamageCalculationResult} object with three fields:
@ -2395,35 +2445,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
};
}
// ----- BEGIN BASE DAMAGE MULTIPLIERS -----
/** A base damage multiplier based on the source's level */
const levelMultiplier = (2 * source.level / 5 + 2);
/** The power of the move after power boosts from abilities, etc. have applied */
const power = move.calculateBattlePower(source, this, simulated);
/**
* The attacker's offensive stat for the given move's category.
* Critical hits ignore negative stat stages.
*/
const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated));
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
/**
* This Pokemon's defensive stat for the given move's category.
* Critical hits ignore positive stat stages.
*/
const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated));
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
/**
* The attack's base damage, as determined by the source's level, move power
* and Attack stat as well as this Pokemon's Defense stat
*/
const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2;
// ------ END BASE DAMAGE MULTIPLIERS ------
const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated);
/** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */
const { targets, multiple } = getMoveTargets(source, move.id);
@ -2549,7 +2575,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// debug message for when damage is applied (i.e. not simulated)
if (!simulated) {
console.log("damage", damage.value, move.name, power, sourceAtk, targetDef);
console.log("damage", damage.value, move.name);
}
let hitResult: HitResult;
@ -3281,7 +3307,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
if (asPhase) {
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText!, sourcePokemon!)); // TODO: are these bangs correct?
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon));
return true;
}
@ -3315,6 +3341,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (effect !== StatusEffect.FAINT) {
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true);
applyPostSetStatusAbAttrs(PostSetStatusAbAttr, this, effect, sourcePokemon);
}
return true;

View File

@ -112,13 +112,13 @@
"island": "PMD EoS Craggy Coast",
"jungle": "Lmz - Jungle",
"laboratory": "Firel - Laboratory",
"lake": "PMD EoS Crystal Cave",
"lake": "Lmz - Lake",
"meadow": "PMD EoS Sky Peak Forest",
"metropolis": "Firel - Metropolis",
"mountain": "PMD EoS Mt. Horn",
"plains": "Firel - Route 888",
"power_plant": "Firel - The Klink",
"ruins": "PMD EoS Deep Sealed Ruin",
"ruins": "Lmz - Ancient Ruins",
"sea": "Andr06 - Marine Mystique",
"seabed": "Firel - Seabed",
"slum": "Andr06 - Sneaky Snom",

View File

@ -9,24 +9,24 @@ import { PokemonPhase } from "./pokemon-phase";
import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase";
export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect: StatusEffect | undefined;
private cureTurn: integer | null;
private sourceText: string | null;
private sourcePokemon: Pokemon | null;
private statusEffect?: StatusEffect | undefined;
private cureTurn?: integer | null;
private sourceText?: string | null;
private sourcePokemon?: Pokemon | null;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string, sourcePokemon?: Pokemon) {
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string | null, sourcePokemon?: Pokemon | null) {
super(scene, battlerIndex);
this.statusEffect = statusEffect;
this.cureTurn = cureTurn!; // TODO: is this bang correct?
this.sourceText = sourceText!; // TODO: is this bang correct?
this.sourcePokemon = sourcePokemon!; // For tracking which Pokemon caused the status effect // TODO: is this bang correct?
this.cureTurn = cureTurn;
this.sourceText = sourceText;
this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect
}
start() {
const pokemon = this.getPokemon();
if (!pokemon?.status) {
if (pokemon?.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
if (pokemon && !pokemon.status) {
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
if (this.cureTurn) {
pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct?
}
@ -40,8 +40,8 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
});
return;
}
} else if (pokemon.status.effect === this.statusEffect) {
this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon)));
} else if (pokemon.status?.effect === this.statusEffect) {
this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)));
}
this.end();
}

View File

@ -0,0 +1,112 @@
import { StatusEffect } from "#app/data/status-effect";
import GameManager from "#app/test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Synchronize", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.SYNCHRONIZE)
.moveset([Moves.SPLASH, Moves.THUNDER_WAVE, Moves.SPORE, Moves.PSYCHO_SHIFT])
.ability(Abilities.NO_GUARD);
}, 20000);
it("does not trigger when no status is applied by opponent Pokemon", async () => {
await game.classicMode.startBattle([Species.FEEBAS]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getParty()[0].status).toBeUndefined();
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}, 20000);
it("sets the status of the source pokemon to Paralysis when paralyzed by it", async () => {
await game.classicMode.startBattle([Species.FEEBAS]);
game.move.select(Moves.THUNDER_WAVE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS);
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS);
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
}, 20000);
it("does not trigger on Sleep", async () => {
await game.classicMode.startBattle();
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getParty()[0].status?.effect).toBeUndefined();
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.SLEEP);
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}, 20000);
it("does not trigger when Pokemon is statused by Toxic Spikes", async () => {
game.override
.ability(Abilities.SYNCHRONIZE)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Array(4).fill(Moves.TOXIC_SPIKES));
await game.classicMode.startBattle([Species.FEEBAS, Species.MILOTIC]);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
game.doSwitchPokemon(1);
await game.phaseInterceptor.to("BerryPhase");
// Assert
expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON);
expect(game.scene.getEnemyParty()[0].status?.effect).toBeUndefined();
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}, 20000);
it("shows ability even if it fails to set the status of the opponent Pokemon", async () => {
await game.classicMode.startBattle([Species.PIKACHU]);
game.move.select(Moves.THUNDER_WAVE);
await game.phaseInterceptor.to("BerryPhase");
// Assert
expect(game.scene.getParty()[0].status?.effect).toBeUndefined();
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS);
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
}, 20000);
it("should activate with Psycho Shift after the move clears the status", async () => {
game.override.statusEffect(StatusEffect.PARALYSIS);
await game.classicMode.startBattle();
game.move.select(Moves.PSYCHO_SHIFT);
await game.phaseInterceptor.to("BerryPhase");
// Assert
expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise
expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS);
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
}, 20000);
});

View File

@ -0,0 +1,87 @@
import { BattlerIndex } from "#app/battle";
import { allMoves, ShellSideArmCategoryAttr } from "#app/data/move";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
describe("Moves - Shell Side Arm", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.SHELL_SIDE_ARM])
.battleType("single")
.startingLevel(100)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("becomes a physical attack if forecasted to deal more damage as physical", async () => {
game.override.enemySpecies(Species.SNORLAX);
await game.classicMode.startBattle([Species.MANAPHY]);
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SHELL_SIDE_ARM);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(true);
}, TIMEOUT);
it("remains a special attack if forecasted to deal more damage as special", async () => {
game.override.enemySpecies(Species.SLOWBRO);
await game.classicMode.startBattle([Species.MANAPHY]);
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SHELL_SIDE_ARM);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false);
}, TIMEOUT);
it("respects stat stage changes when forecasting base damage", async () => {
game.override
.enemySpecies(Species.SNORLAX)
.enemyMoveset(Moves.COTTON_GUARD);
await game.classicMode.startBattle([Species.MANAPHY]);
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SHELL_SIDE_ARM);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false);
}, TIMEOUT);
});

View File

@ -344,6 +344,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
super.clear();
this.config = null;
this.optionSelectContainer.setVisible(false);
this.scrollCursor = 0;
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 i18next from "i18next";
import { Achv, achvs, getAchievementDescription } from "../system/achv";
import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "../system/voucher";
import MessageUiHandler from "./message-ui-handler";
import { addTextObject, TextStyle } from "./text";
import { Mode } from "./ui";
import { addWindow } from "./ui-theme";
import { Achv, achvs, getAchievementDescription } from "#app/system/achv";
import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#app/system/voucher";
import MessageUiHandler from "#app/ui/message-ui-handler";
import { addTextObject, TextStyle } from "#app/ui/text";
import { Mode } from "#app/ui/ui";
import { addWindow } from "#app/ui/ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar";
import { PlayerGender } from "#enums/player-gender";
enum Page {
@ -49,6 +50,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
private vouchersTotal: number;
private currentTotal: number;
private scrollBar: ScrollBar;
private scrollCursor: number;
private cursorObj: Phaser.GameObjects.NineSlice | null;
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.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 = [];
@ -148,6 +153,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.mainContainer.add(this.headerText);
this.mainContainer.add(this.headerActionText);
this.mainContainer.add(this.iconsBg);
this.mainContainer.add(this.scrollBar);
this.mainContainer.add(this.iconsContainer);
this.mainContainer.add(titleBg);
this.mainContainer.add(this.titleText);
@ -162,6 +168,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.currentPage = Page.ACHIEVEMENTS;
this.setCursor(0);
this.setScrollCursor(0);
this.mainContainer.setVisible(false);
}
@ -175,6 +182,8 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.mainContainer.setVisible(true);
this.setCursor(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);
@ -224,6 +233,8 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.updateAchvIcons();
}
this.setCursor(0, true);
this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS));
this.scrollBar.setScrollCursor(0);
this.mainContainer.update();
}
if (button === Button.CANCEL) {
@ -237,32 +248,44 @@ export default class AchvsUiHandler extends MessageUiHandler {
if (this.cursor < this.COLS) {
if (this.scrollCursor) {
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 {
success = this.setCursor(this.cursor - this.COLS);
}
break;
case Button.DOWN:
const canMoveDown = (this.cursor + itemOffset) + this.COLS < this.currentTotal;
const canMoveDown = itemOffset + 1 < this.currentTotal;
if (rowIndex >= this.ROWS - 1) {
if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) {
// scroll down one row
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) {
success = this.setCursor(this.cursor + this.COLS);
success = this.setCursor(Math.min(this.cursor + this.COLS, this.currentTotal - itemOffset - 1));
}
break;
case Button.LEFT:
if (!this.cursor && this.scrollCursor) {
success = this.setScrollCursor(this.scrollCursor - 1) && this.setCursor(this.cursor + (this.COLS - 1));
} else if (this.cursor) {
if (this.cursor % this.COLS === 0) {
success = this.setCursor(Math.min(this.cursor + this.COLS - 1, this.currentTotal - itemOffset - 1));
} else {
success = this.setCursor(this.cursor - 1);
}
break;
case Button.RIGHT:
if (this.cursor + 1 === this.ROWS * this.COLS && this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS) {
success = this.setScrollCursor(this.scrollCursor + 1) && this.setCursor(this.cursor - (this.COLS - 1));
} else if (this.cursor + itemOffset < this.currentTotal - 1) {
if ((this.cursor + 1) % this.COLS === 0 || (this.cursor + itemOffset) === (this.currentTotal - 1)) {
success = this.setCursor(this.cursor - this.cursor % this.COLS);
} else {
success = this.setCursor(this.cursor + 1);
}
break;
@ -315,15 +338,22 @@ export default class AchvsUiHandler extends MessageUiHandler {
}
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) {
case Page.ACHIEVEMENTS:
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;
case Page.VOUCHERS:
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;
}
return true;
@ -411,6 +441,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
super.clear();
this.currentPage = Page.ACHIEVEMENTS;
this.mainContainer.setVisible(false);
this.setScrollCursor(0);
this.eraseCursor();
}

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 {
private bg: Phaser.GameObjects.Image;
private bg: Phaser.GameObjects.NineSlice;
private handleBody: Phaser.GameObjects.Rectangle;
private handleBottom: Phaser.GameObjects.Image;
private pages: number;
private page: number;
private handleBottom: Phaser.GameObjects.NineSlice;
private currentRow: 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);
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.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.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.add(this.handleBottom);
}
setPage(page: number): void {
this.page = page;
this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.pages * page;
/**
* Set the current row that is displayed
* 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;
}
setPages(pages: number): void {
this.pages = pages;
this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * 9 / this.pages;
/**
* Set the total number of rows to display
* 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 BattleScene from "../../battle-scene";
import {Mode} from "../ui";
import {InterfaceConfig} from "../../inputs-controller";
import {addWindow} from "../ui-theme";
import {addTextObject, TextStyle} from "../text";
import {getIconWithSettingName} from "#app/configs/inputs/configHandler";
import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu";
import UiHandler from "#app/ui/ui-handler";
import BattleScene from "#app/battle-scene";
import { Mode } from "#app/ui/ui";
import { InterfaceConfig } from "#app/inputs-controller";
import { addWindow } from "#app/ui/ui-theme";
import { addTextObject, TextStyle } from "#app/ui/text";
import { ScrollBar } from "#app/ui/scroll-bar";
import { getIconWithSettingName } from "#app/configs/inputs/configHandler";
import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu";
import { Device } from "#enums/devices";
import { Button } from "#enums/buttons";
import i18next from "i18next";
@ -19,7 +20,7 @@ export interface LayoutConfig {
inputsIcons: InputsIcons;
settingLabels: Phaser.GameObjects.Text[];
optionValueLabels: Phaser.GameObjects.Text[][];
optionCursors: integer[];
optionCursors: number[];
keys: string[];
bindingSettings: Array<String>;
}
@ -31,8 +32,9 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
protected optionsContainer: Phaser.GameObjects.Container;
protected navigationContainer: NavigationMenu;
protected scrollCursor: integer;
protected optionCursors: integer[];
protected scrollBar: ScrollBar;
protected scrollCursor: number;
protected optionCursors: number[];
protected cursorObj: Phaser.GameObjects.NineSlice | null;
protected optionsBg: Phaser.GameObjects.NineSlice;
@ -65,7 +67,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
protected device: Device;
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.
@ -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
// 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
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.
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.
ui.add(this.settingsContainer);
@ -413,6 +420,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
this.optionCursors = layout.optionCursors;
this.inputsIcons = layout.inputsIcons;
this.bindingSettings = layout.bindingSettings;
this.scrollBar.setTotalRows(layout.settingLabels.length);
this.scrollBar.setScrollCursor(0);
// Return true indicating the layout was successfully applied.
return true;
@ -538,7 +547,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param cursor - The cursor position to set.
* @returns `true` if the cursor was set successfully.
*/
setCursor(cursor: integer): boolean {
setCursor(cursor: number): boolean {
const ret = super.setCursor(cursor);
// If the optionsContainer is not initialized, return the result from the parent class directly.
if (!this.optionsContainer) {
@ -547,7 +556,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Check if the cursor object exists, if not, create it.
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.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.
* @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.
if (scrollCursor === this.scrollCursor) {
return false;
@ -572,6 +582,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Update the internal scroll cursor state
this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
// Apply the new scroll position to the settings UI.
this.updateSettingsScroll();
@ -590,7 +601,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param save - Whether to save the setting to local storage.
* @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.
const setting = this.setting[Object.keys(this.setting)[settingIndex]];

View File

@ -1,12 +1,13 @@
import BattleScene from "../../battle-scene";
import { hasTouchscreen, isMobile } from "../../touch-controls";
import { TextStyle, addTextObject } from "../text";
import { Mode } from "../ui";
import UiHandler from "../ui-handler";
import { addWindow } from "../ui-theme";
import {Button} from "#enums/buttons";
import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler";
import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu";
import BattleScene from "#app/battle-scene";
import { hasTouchscreen, isMobile } from "#app/touch-controls";
import { TextStyle, addTextObject } from "#app/ui/text";
import { Mode } from "#app/ui/ui";
import UiHandler from "#app/ui/ui-handler";
import { addWindow } from "#app/ui/ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar";
import { Button } from "#enums/buttons";
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 i18next from "i18next";
@ -19,11 +20,12 @@ export default class AbstractSettingsUiHandler extends UiHandler {
private optionsContainer: Phaser.GameObjects.Container;
private navigationContainer: NavigationMenu;
private scrollCursor: integer;
private scrollCursor: number;
private scrollBar: ScrollBar;
private optionsBg: Phaser.GameObjects.NineSlice;
private optionCursors: integer[];
private optionCursors: number[];
private settingLabels: 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 totalSpace = (300 - labelWidth) - totalWidth / 6;
const totalSpace = (297 - labelWidth) - totalWidth / 6;
const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1));
let xOffset = 0;
@ -130,7 +132,11 @@ export default class AbstractSettingsUiHandler extends UiHandler {
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.scrollBar);
this.settingsContainer.add(this.navigationContainer);
this.settingsContainer.add(actionsBg);
this.settingsContainer.add(this.optionsContainer);
@ -186,6 +192,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.settingsContainer.setVisible(true);
this.setCursor(0);
this.setScrollCursor(0);
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.
* @returns `true` if the cursor was set successfully.
*/
setCursor(cursor: integer): boolean {
setCursor(cursor: number): boolean {
const ret = super.setCursor(cursor);
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.optionsContainer.add(this.cursorObj);
}
@ -323,7 +331,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param save - Whether to save the setting to local storage.
* @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];
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.
* @returns `true` if the scroll cursor was set successfully.
*/
setScrollCursor(scrollCursor: integer): boolean {
setScrollCursor(scrollCursor: number): boolean {
if (scrollCursor === this.scrollCursor) {
return false;
}
this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
this.updateSettingsScroll();
@ -394,6 +403,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
clear() {
super.clear();
this.settingsContainer.setVisible(false);
this.setScrollCursor(0);
this.eraseCursor();
this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar);
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
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);
@ -2540,8 +2540,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
});
this.starterSelectScrollBar.setPages(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1));
this.starterSelectScrollBar.setPage(0);
this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1));
this.starterSelectScrollBar.setScrollCursor(0);
// sort
const sort = this.filterBar.getVals(DropDownColumn.SORT)[0];
@ -2576,7 +2576,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const onScreenFirstIndex = this.scrollCursor * maxColumns;
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;
this.filteredStarterContainers.forEach((container, i) => {