Compare commits

...

6 Commits

Author SHA1 Message Date
Opaque02 67d0ae7e2a
Merge 5f562ba4b9 into 51bb80cb66 2024-09-19 02:03:47 -04: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
peng06 0567d4b5e2
[Dev] Remove `variant-tiers.ts` Enum (#3746)
* remove varianttiers

* Thanks GitHub

* Update import in `overrides.ts`

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com>
2024-09-18 15:20:33 -04:00
flx-sta 6030b780f2
[Move][Mirror] Update HitTagAttr attributes v2 (#4297)
* [Move] Updated HitAttr tags

Affects Whirlwind/Fly, Steamroller/Minimize, and Malicious Moonsault/Minimize

* [Move] Update for MinimizeAccuracyAttr

Affects Steamroller and Malicious Moonsault

* add: whirlwind test

* add: steamroller test

* rename: `AlwaysHitMinimizeAttr` (from `MinimizeAccuracyAttr`)

* rename: `DealsDoubleDamageToTagAttr` (from `HitsTagAttr`)

---------

Co-authored-by: chaosgrimmon <31082757+chaosgrimmon@users.noreply.github.com>
2024-09-17 22:41:46 -04:00
25 changed files with 460 additions and 157 deletions

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,6 @@
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./pokemon-species"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./pokemon-species";
import { VariantTier } from "../enums/variant-tiers"; import { VariantTier } from "../enums/variant-tier";
import * as Utils from "../utils"; import * as Utils from "../utils";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { pokemonPrevolutions } from "./pokemon-evolutions"; import { pokemonPrevolutions } from "./pokemon-evolutions";
@ -178,7 +178,7 @@ export class Egg {
// be done because species with no variants get filtered at rollSpecies but if the // be done because species with no variants get filtered at rollSpecies but if the
// species is set via options or the legendary gacha pokemon gets choosen the check never happens // species is set via options or the legendary gacha pokemon gets choosen the check never happens
if (this._species && !getPokemonSpecies(this._species).hasVariants()) { if (this._species && !getPokemonSpecies(this._species).hasVariants()) {
this._variantTier = VariantTier.COMMON; this._variantTier = VariantTier.STANDARD;
} }
// Needs this._tier so it needs to be generated afer the tier override if bought from same species // Needs this._tier so it needs to be generated afer the tier override if bought from same species
this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex(); this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex();
@ -494,12 +494,12 @@ export class Egg {
// place but I don't want to touch the pokemon class. // place but I don't want to touch the pokemon class.
private rollVariant(): VariantTier { private rollVariant(): VariantTier {
if (!this.isShiny) { if (!this.isShiny) {
return VariantTier.COMMON; return VariantTier.STANDARD;
} }
const rand = Utils.randSeedInt(10); const rand = Utils.randSeedInt(10);
if (rand >= 4) { if (rand >= 4) {
return VariantTier.COMMON; // 6/10 return VariantTier.STANDARD; // 6/10
} else if (rand >= 1) { } else if (rand >= 1) {
return VariantTier.RARE; // 3/10 return VariantTier.RARE; // 3/10
} else { } else {

View File

@ -3839,7 +3839,7 @@ export class StormAccuracyAttr extends VariableAccuracyAttr {
* @extends VariableAccuracyAttr * @extends VariableAccuracyAttr
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class MinimizeAccuracyAttr extends VariableAccuracyAttr { export class AlwaysHitMinimizeAttr extends VariableAccuracyAttr {
/** /**
* @see {@linkcode apply} * @see {@linkcode apply}
* @param user N/A * @param user N/A
@ -3974,18 +3974,17 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr {
export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr { export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.NumberHolder); 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 const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true);
if (atkRatio > specialRatio) { const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true);
if (predictedPhysDmg > predictedSpecDmg) {
category.value = MoveCategory.PHYSICAL; category.value = MoveCategory.PHYSICAL;
return true; return true;
} else if (atkRatio === specialRatio && user.randSeedInt(2) === 0) { } else if (predictedPhysDmg === predictedSpecDmg && user.randSeedInt(2) === 0) {
category.value = MoveCategory.PHYSICAL; category.value = MoveCategory.PHYSICAL;
return true; return true;
} }
return false; return false;
} }
} }
@ -4855,10 +4854,10 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr {
* Attribute used when a move hits a {@linkcode BattlerTagType} for double damage * Attribute used when a move hits a {@linkcode BattlerTagType} for double damage
* @extends MoveAttr * @extends MoveAttr
*/ */
export class HitsTagAttr extends MoveAttr { export class DealsDoubleDamageToTagAttr extends MoveAttr {
/** The {@linkcode BattlerTagType} this move hits */ /** The {@linkcode BattlerTagType} this move hits */
public tagType: BattlerTagType; public tagType: BattlerTagType;
/** Should this move deal double damage against {@linkcode HitsTagAttr.tagType}? */ /** Should this move deal double damage against {@linkcode DealsDoubleDamageToTagAttr.tagType}? */
public doubleDamage: boolean; public doubleDamage: boolean;
constructor(tagType: BattlerTagType, doubleDamage?: boolean) { constructor(tagType: BattlerTagType, doubleDamage?: boolean) {
@ -6752,12 +6751,11 @@ export function initMoves() {
new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1) new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1)
.slicingMove(), .slicingMove(),
new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1) new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1)
.attr(HitsTagAttr, BattlerTagType.FLYING, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true)
.windMove(), .windMove(),
new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1), new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1),
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1) new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr) .attr(ForceSwitchOutAttr)
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
.ignoresSubstitute() .ignoresSubstitute()
.hidesTarget() .hidesTarget()
.windMove(), .windMove(),
@ -6770,8 +6768,8 @@ export function initMoves() {
new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1), new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1),
new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1), new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1),
new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1) new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1) new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1)
.attr(MultiHitAttr, MultiHitType._2), .attr(MultiHitAttr, MultiHitType._2),
@ -6795,8 +6793,8 @@ export function initMoves() {
.attr(OneHitKOAccuracyAttr), .attr(OneHitKOAccuracyAttr),
new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1),
new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1) new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1) new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1)
.attr(TrapAttr, BattlerTagType.WRAP), .attr(TrapAttr, BattlerTagType.WRAP),
@ -6864,7 +6862,7 @@ export function initMoves() {
new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1), new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1),
new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1) new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.attr(HitsTagAttr, BattlerTagType.UNDERWATER, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true)
.attr(GulpMissileTagAttr), .attr(GulpMissileTagAttr),
new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
.attr(StatusEffectAttr, StatusEffect.FREEZE), .attr(StatusEffectAttr, StatusEffect.FREEZE),
@ -6947,18 +6945,18 @@ export function initMoves() {
new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1) new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.attr(ThunderAccuracyAttr) .attr(ThunderAccuracyAttr)
.attr(HitsTagAttr, BattlerTagType.FLYING, false), .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false),
new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1) new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1) new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1)
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
.attr(OneHitKOAttr) .attr(OneHitKOAttr)
.attr(OneHitKOAccuracyAttr) .attr(OneHitKOAccuracyAttr)
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, false)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND) .attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND)
@ -7347,7 +7345,7 @@ export function initMoves() {
.attr(PreMoveMessageAttr, magnitudeMessageFunc) .attr(PreMoveMessageAttr, magnitudeMessageFunc)
.attr(MagnitudePowerAttr) .attr(MagnitudePowerAttr)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2) new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2)
@ -7403,7 +7401,7 @@ export function initMoves() {
new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2) new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2)
.attr(HighCritAttr), .attr(HighCritAttr),
new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2) new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2)
.attr(HitsTagAttr, BattlerTagType.FLYING, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true)
.attr(FlinchAttr) .attr(FlinchAttr)
.windMove() .windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -7435,7 +7433,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1), .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2) new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
.attr(TrapAttr, BattlerTagType.WHIRLPOOL) .attr(TrapAttr, BattlerTagType.WHIRLPOOL)
.attr(HitsTagAttr, BattlerTagType.UNDERWATER, true), .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true),
new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2) new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2)
.attr(MultiHitAttr, MultiHitType.BEAT_UP) .attr(MultiHitAttr, MultiHitType.BEAT_UP)
.attr(BeatUpAttr) .attr(BeatUpAttr)
@ -7658,7 +7656,7 @@ export function initMoves() {
new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3) new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3) new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3)
.attr(HitsTagAttr, BattlerTagType.FLYING) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING)
.punchingMove(), .punchingMove(),
new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3) new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
.attr(TrapAttr, BattlerTagType.SAND_TOMB) .attr(TrapAttr, BattlerTagType.SAND_TOMB)
@ -7889,8 +7887,8 @@ export function initMoves() {
new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4) new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4)
.pulseMove(), .pulseMove(),
new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4) new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4), new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4),
new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4) new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4)
@ -8087,7 +8085,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
.attr(HitsTagAttr, BattlerTagType.FLYING, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.attr(CritOnlyAttr), .attr(CritOnlyAttr),
@ -8100,9 +8098,9 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.danceMove(), .danceMove(),
new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(CompareWeightPowerAttr) .attr(CompareWeightPowerAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true), .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true),
new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5) new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.condition(unknownTypeCondition) .condition(unknownTypeCondition)
@ -8253,12 +8251,14 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.slicingMove(), .slicingMove(),
new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(CompareWeightPowerAttr) .attr(CompareWeightPowerAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true), .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true),
new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5) new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5) new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5)
.attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.attr(FlinchAttr), .attr(FlinchAttr),
new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5) new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.DEF ], 3, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 3, true),
@ -8271,7 +8271,7 @@ export function initMoves() {
new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5) new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5)
.attr(ThunderAccuracyAttr) .attr(ThunderAccuracyAttr)
.attr(ConfuseAttr) .attr(ConfuseAttr)
.attr(HitsTagAttr, BattlerTagType.FLYING, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false)
.windMove(), .windMove(),
new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5) new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5)
.attr(RecoilAttr) .attr(RecoilAttr)
@ -8325,9 +8325,9 @@ export function initMoves() {
.attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE) .attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6) new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6)
.attr(MinimizeAccuracyAttr) .attr(AlwaysHitMinimizeAttr)
.attr(FlyingTypeMultiplierAttr) .attr(FlyingTypeMultiplierAttr)
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.condition(failOnGravityCondition), .condition(failOnGravityCondition),
new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6) new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
@ -8498,8 +8498,8 @@ export function initMoves() {
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr) .attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
.attr(HitsTagAttr, BattlerTagType.FLYING, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false)
.attr(HitsTagAttr, BattlerTagType.MAGNET_RISEN, false) .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MAGNET_RISEN, false)
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
.makesContact(false) .makesContact(false)
@ -8756,6 +8756,8 @@ export function initMoves() {
.partial() .partial()
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7)
.attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
.partial() .partial()
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7)
@ -9103,7 +9105,7 @@ export function initMoves() {
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
.attr(ShellSideArmCategoryAttr) .attr(ShellSideArmCategoryAttr)
.attr(StatusEffectAttr, StatusEffect.POISON) .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) new AttackMove(Moves.MISTY_EXPLOSION, Type.FAIRY, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8)
.attr(SacrificialAttr) .attr(SacrificialAttr)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)

View File

@ -1,5 +0,0 @@
export enum VariantTier {
COMMON,
RARE,
EPIC
}

View File

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

View File

@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant"; import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "../data/move"; import Move, { HighCritAttr, DealsDoubleDamageToTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "../data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
@ -1513,7 +1513,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType); const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType);
for (const tag of immuneTags) { for (const tag of immuneTags) {
if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) { if (move && !move.getAttrs(DealsDoubleDamageToTagAttr).some(attr => attr.tagType === tag.tagType)) {
typeMultiplier.value = 0; typeMultiplier.value = 0;
break; break;
} }
@ -2322,11 +2322,61 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return accuracyMultiplier.value / evasionMultiplier.value; 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 * Calculates the damage of an attack made by another Pokemon against this Pokemon
* @param source {@linkcode Pokemon} the attacking Pokemon * @param source {@linkcode Pokemon} the attacking Pokemon
* @param move {@linkcode Pokemon} the move used in the attack * @param move {@linkcode Pokemon} the move used in the attack
* @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects * @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 isCritical If `true`, calculates damage for a critical hit.
* @param simulated If `true`, suppresses changes to game state during the calculation. * @param simulated If `true`, suppresses changes to game state during the calculation.
* @returns a {@linkcode DamageCalculationResult} object with three fields: * @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 * 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 * and Attack stat as well as this Pokemon's Defense stat
*/ */
const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2; const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated);
// ------ END BASE DAMAGE MULTIPLIERS ------
/** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */ /** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */
const { targets, multiple } = getMoveTargets(source, move.id); const { targets, multiple } = getMoveTargets(source, move.id);
@ -2489,13 +2515,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier);
/** /**
* For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if: * For each {@linkcode DealsDoubleDamageToTagAttr} the move has, doubles the damage of the move if:
* The target has a {@linkcode BattlerTagType} that this move interacts with * The target has a {@linkcode BattlerTagType} that this move interacts with
* AND * AND
* The move doubles damage when used against that tag * The move doubles damage when used against that tag
*/ */
const hitsTagMultiplier = new Utils.NumberHolder(1); const hitsTagMultiplier = new Utils.NumberHolder(1);
move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { move.getAttrs(DealsDoubleDamageToTagAttr).filter(hta => hta.doubleDamage).forEach(hta => {
if (this.getTag(hta.tagType)) { if (this.getTag(hta.tagType)) {
hitsTagMultiplier.value *= 2; hitsTagMultiplier.value *= 2;
} }
@ -2549,7 +2575,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// debug message for when damage is applied (i.e. not simulated) // debug message for when damage is applied (i.e. not simulated)
if (!simulated) { if (!simulated) {
console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); console.log("damage", damage.value, move.name);
} }
let hitResult: HitResult; let hitResult: HitResult;

View File

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

View File

@ -6,7 +6,7 @@ import { PokeballType } from "#enums/pokeball";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import { VariantTier } from "#enums/variant-tiers"; import { VariantTier } from "#enums/variant-tier";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { type PokeballCounts } from "./battle-scene"; import { type PokeballCounts } from "./battle-scene";
import { Gender } from "./data/gender"; import { Gender } from "./data/gender";

View File

@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move"; import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, DealsDoubleDamageToTagAttr } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
@ -394,7 +394,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
if (semiInvulnerableTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) { if (semiInvulnerableTag && !this.move.getMove().getAttrs(DealsDoubleDamageToTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) {
return false; return false;
} }

View File

@ -1,6 +1,6 @@
import { EggTier } from "#enums/egg-type"; import { EggTier } from "#enums/egg-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { VariantTier } from "#enums/variant-tiers"; import { VariantTier } from "#enums/variant-tier";
import { EGG_SEED, Egg } from "../data/egg"; import { EGG_SEED, Egg } from "../data/egg";
import { EggSourceType } from "#app/enums/egg-source-types"; import { EggSourceType } from "#app/enums/egg-source-types";

View File

@ -1,7 +1,7 @@
import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg"; import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg";
import { EggSourceType } from "#app/enums/egg-source-types"; import { EggSourceType } from "#app/enums/egg-source-types";
import { EggTier } from "#app/enums/egg-type"; import { EggTier } from "#app/enums/egg-type";
import { VariantTier } from "#app/enums/variant-tiers"; import { VariantTier } from "#app/enums/variant-tier";
import EggData from "#app/system/egg-data"; import EggData from "#app/system/egg-data";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -136,9 +136,9 @@ describe("Egg Generation Tests", () => {
expect(result).toBe(expectedResult); expect(result).toBe(expectedResult);
}); });
it("should return a shiny common variant", () => { it("should return a shiny standard variant", () => {
const scene = game.scene; const scene = game.scene;
const expectedVariantTier = VariantTier.COMMON; const expectedVariantTier = VariantTier.STANDARD;
const result = new Egg({ scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR }).generatePlayerPokemon(scene).variant; const result = new Egg({ scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR }).generatePlayerPokemon(scene).variant;

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

@ -0,0 +1,58 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { DamageCalculationResult } from "#app/field/pokemon";
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, expect, it, vi } from "vitest";
describe("Moves - Steamroller", () => {
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.moveset([Moves.STEAMROLLER]).battleType("single").enemyAbility(Abilities.BALL_FETCH);
});
it("should always hit a minimzed target with double damage", async () => {
game.override.enemySpecies(Species.DITTO).enemyMoveset(Moves.MINIMIZE);
await game.classicMode.startBattle([Species.IRON_BOULDER]);
const ditto = game.scene.getEnemyPokemon()!;
vi.spyOn(ditto, "getAttackDamage");
ditto.hp = 5000;
const steamroller = allMoves[Moves.STEAMROLLER];
vi.spyOn(steamroller, "calculateBattleAccuracy");
const ironBoulder = game.scene.getPlayerPokemon()!;
vi.spyOn(ironBoulder, "getAccuracyMultiplier");
// Turn 1
game.move.select(Moves.STEAMROLLER);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn();
// Turn 2
game.move.select(Moves.STEAMROLLER);
await game.toNextTurn();
const [dmgCalcTurn1, dmgCalcTurn2]: DamageCalculationResult[] = vi
.mocked(ditto.getAttackDamage)
.mock.results.map((r) => r.value);
expect(dmgCalcTurn2.damage).toBeGreaterThanOrEqual(dmgCalcTurn1.damage * 2);
expect(ditto.getTag(BattlerTagType.MINIMIZED)).toBeDefined();
expect(steamroller.calculateBattleAccuracy).toHaveReturnedWith(-1);
});
});

View File

@ -0,0 +1,53 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { BattlerTagType } from "#app/enums/battler-tag-type";
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 - Whirlwind", () => {
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")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.WHIRLWIND)
.enemySpecies(Species.PIDGEY);
});
it.each([
{ move: Moves.FLY, name: "Fly" },
{ move: Moves.BOUNCE, name: "Bounce" },
{ move: Moves.SKY_DROP, name: "Sky Drop" },
])("should not hit a flying target: $name (=$move)", async ({ move }) => {
game.override.moveset([move]);
await game.classicMode.startBattle([Species.STARAPTOR]);
const staraptor = game.scene.getPlayerPokemon()!;
const whirlwind = allMoves[Moves.WHIRLWIND];
vi.spyOn(whirlwind, "getFailedText");
game.move.select(move);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn();
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
expect(whirlwind.getFailedText).toHaveBeenCalledOnce();
});
});

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

@ -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) => {