Compare commits

...

19 Commits

Author SHA1 Message Date
Christopher Schmidt
f159b2e84d Adds unit tests for sparkly swirl 2024-08-30 13:09:17 -04:00
Christopher Schmidt
170abd1dc7 Merge branch 'sparklyswirlbug2' of https://github.com/schmidtc1/pokerogue into sparklyswirlbug2 2024-08-30 11:57:26 -04:00
schmidtc1
a3c60c43b2
Merge branch 'beta' into sparklyswirlbug2 2024-08-30 11:56:58 -04:00
Mumble
60aa61e56e
[Bug] Skip Eternatus dialogue again (#3716)
* The fix.

* Ordinals....

* tsdocs..

* my forgetting

* Fixed issues

* Update src/phases/encounter-phase.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* The actual change

* actual fixes

---------

Co-authored-by: frutescens <info@laptop>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2024-08-30 11:16:15 +02:00
chaosgrimmon
db4a63dbb9
[Sprite] Fix Courtney eye whites (#3907) 2024-08-30 00:15:35 -07:00
Madmadness65
aeecb67f32 Add inverse "item" sprite
Pull #3525 forgot to add this graphic into the items folder, which would mean the sprite would be lost if the items atlas was overwritten at any point. This shouldn't affect any existing functionality.
2024-08-29 20:56:05 -05:00
Opaque02
d1132a5765
[QoL] Test dialogue menu option (#3725)
* Adding code to allow use of a testing dialogue translation menu

* Updated to include autocomplete functionality

* Added multiple inputs

* Added locales for other languages as well as checks to make this only available on local/beta

* Updated a few things to try get the dialogue to work for full length of the window

* Fixed issue with message box not taking up full length of the screen (thanks Moka!) and some minor bugs

* Whoops, forgot to stage a file

* Updated locale files

* Fixed broken tests and docs

* Removed keys from json

* Reordered and reorganised some things

* Put admin enum at end to match handlers

* Removed old unneeded line of code

* Updated to include the ability to handle cases where i18 keys are null in the locales json
2024-08-30 02:38:46 +01:00
Frederico Santos
f7169868f3 chore: Refactor AdminUiHandler to clear input fields and revert mode 2024-08-30 00:22:09 +01:00
Frederico Santos
7eb6ba4dfd chore: Clear input fields and revert mode in AdminUiHandler 2024-08-30 00:19:03 +01:00
Frederico Santos
f1111dc0d2 chore: Update AdminUiHandler to clear input fields and set mode to ADMIN 2024-08-30 00:13:53 +01:00
AJ Fontaine
3b9b0c4091
Blitzy's implementation of evil teams in trainer-config.ts (#3884) 2024-08-30 00:05:09 +01:00
Frederico Santos
8bf44a2047 Fix error handling and revert mode in AdminUiHandler 2024-08-29 23:59:19 +01:00
MokaStitcher
e4da48f51a
[Bug] Starter select default attributes fixes (#3870)
* Test changes to starter ui stuff for edge case stuff to fix

* Minor bug fixes

* [starter-ui] cleanup outdated fix

* use existing method to get the default form from the caught attributes

* clear the existing StarterPreferences of potential invalid values

* remember the last variant used even when disabling shiny form

* fix variant and shiny checks for edge case with variant flags but no shiny flag

* more fixes for invalid starter prefs and default settings

---------

Co-authored-by: Opaque02 <66582645+Opaque02@users.noreply.github.com>
Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com>
2024-08-29 14:47:37 -07:00
AJ Fontaine
b2cd21bcb1
Fix evolution items not working on mons without forms (#3902) 2024-08-29 22:13:10 +01:00
Mumble
9833b1da7e
[Balance] Randomized Biome after End Biome (#3888)
* Biome.END goes somewhere random

* this way island is included too

* no towns or plains

* Enums are dumb

---------

Co-authored-by: frutescens <info@laptop>
2024-08-29 22:10:38 +01:00
damocleas
db9434ac11
[Balance] Biome Changes / Minor Passive Changes / Minor Egg Move Changes / Beta GMax form adjustments (#3852)
* [Balance] Previous Egg/Passive/Eternatus Update Adjustments

* Update pokemon-species.ts

* Updated Eternatus, src/field/pokemon.ts

* Update egg-moves.ts for Drowzee and Darkrai

* Update biomes.ts

* Update biomes.ts to screw over Dojo!

* Update pokemon-species.ts gmax adjustments
2024-08-29 20:29:06 +01:00
Leo Kim
c112abbcd2
[Challenge] Inverse battle challenge (#3525)
* add inverse battle challenge. refactoring type.ts for inverse battle challenge

* update type integer -> number

* add inverse battle condition to thunder wave, conversion 2.

* add inverse_battle test code, add checking gameMode in runToSummon not to overwrite gameMode to CLASSIC always

* update startBattle with isClassicMode default = true

* add inverse achievement

* fix achv validation condition

* remove unnecessary new line

* update defaultWidth 160 -> 200

* update locales

* fix korean translation

* fix korean translation2

* Update src/locales/de/achv.ts

Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>

* Update src/locales/de/challenges.ts

Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>

* Update src/locales/de/challenges.ts

Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>

* resize challenge description 96 -> 84

* update challenge select UI size.

* revert font size to 84. update de translation

* Update src/locales/fr/challenges.ts

Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr>

* Update src/locales/fr/achv.ts

Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr>

* Update src/locales/es/challenges.ts

Co-authored-by: Asdar <asdargmng@gmail.com>

* Update src/locales/fr/challenges.ts

Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr>

* Update src/locales/fr/achv.ts

Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr>

* Update src/locales/es/achv.ts

Co-authored-by: Asdar <asdargmng@gmail.com>

* Update src/locales/fr/achv.ts

Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr>

* shrink de font size on achivement

* set middle align to achv title

* Update src/locales/zh_CN/achv.ts

Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com>

* Update src/locales/zh_TW/achv.ts

Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com>

* Update src/locales/zh_CN/challenges.ts

Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com>

* Update src/locales/zh_TW/challenges.ts

Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com>

* fix zh_TW ahiv.ts

* fix import code on inverse battle test for updated phase

* Update src/data/type.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* update requested changes

* Update src/locales/pt_BR/achv.ts

Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br>

* Update src/locales/pt_BR/achv.ts

Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br>

* Update src/locales/pt_BR/challenges.ts

Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br>

* [draft] update inverse battle apply function

* change the way how to use applyChallenge for inverse type

* resolve confilct

* fix test codes

* remove unnecessary multiplier variable and break codes

* update getTypeDamageMultiplier argument type from `number` to `Type`

* Fix inverse types tests (#1)

* Fix Inverse Battle tests

* Add timeout parameter to tests

* update requested changes

* update requested changes

* update requested changes2

* update comments

* Update src/test/utils/helpers/challengeModeHelper.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update src/test/utils/helpers/challengeModeHelper.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* fix mis pasted code

* revert loadChallenge code for  FreshStartChallenge

* code refactoring

* restore challenge.json lost translations

* revert UI changes

* revert unreverted newlines

* Run History inclusion

* requested changes from torranx

* update WaterSuperEffectTypeMultiplierAttr for inverse battle matchup.

* fix test code. adding flying press test code

* update requested change from xavion3

* updated requested change from xavion 2

* update requested changes from xavion 3

* remove exception code which is not valid

* attach partial mark to Freeze dry. requested by xavion

* add missing game over phase code when we delete old phases.ts

* fix test codes

* merge conflict

* fix achv condition

* updated achv block condition. we don't want to change desc now

* resolve conflict

* Eternatus Moveset Tinkering

* Cleaning it up

---------

Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>
Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr>
Co-authored-by: Asdar <asdargmng@gmail.com>
Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
Co-authored-by: frutescens <info@laptop>
2024-08-29 19:59:33 +01:00
Christopher Schmidt
e2503d0d3e Merge branch 'sparklyswirlbug2' of https://github.com/schmidtc1/pokerogue into sparklyswirlbug2 2024-08-29 13:50:13 -04:00
Christopher Schmidt
3eb9215985 Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into sparklyswirlbug2 2024-08-29 13:46:11 -04:00
50 changed files with 6722 additions and 6064 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 888 B

View File

@ -35,36 +35,36 @@ interface BiomeDepths {
export const biomeLinks: BiomeLinks = {
[Biome.TOWN]: Biome.PLAINS,
[Biome.PLAINS]: [ Biome.GRASS, Biome.METROPOLIS, Biome.LAKE ],
[Biome.GRASS]: Biome.TALL_GRASS,
[Biome.GRASS]: [ Biome.TALL_GRASS, [ Biome.CONSTRUCTION_SITE, 2 ] ],
[Biome.TALL_GRASS]: [ Biome.FOREST, Biome.CAVE ],
[Biome.SLUM]: Biome.CONSTRUCTION_SITE,
[Biome.FOREST]: [ Biome.JUNGLE, Biome.MEADOW ],
[Biome.SEA]: [ Biome.SEABED, Biome.ICE_CAVE ],
[Biome.SWAMP]: [ Biome.GRAVEYARD, Biome.TALL_GRASS ],
[Biome.BEACH]: [ Biome.SEA, [ Biome.ISLAND, 4 ] ],
[Biome.BEACH]: [ Biome.SEA, [ Biome.ISLAND, 3 ] ],
[Biome.LAKE]: [ Biome.BEACH, Biome.SWAMP, Biome.CONSTRUCTION_SITE ],
[Biome.SEABED]: [ Biome.CAVE, [ Biome.VOLCANO, 4 ] ],
[Biome.MOUNTAIN]: [ Biome.VOLCANO, [ Biome.WASTELAND, 3 ] ],
[Biome.SEABED]: [ Biome.CAVE, [ Biome.VOLCANO, 3 ] ],
[Biome.MOUNTAIN]: [ Biome.VOLCANO, [ Biome.DOJO, 2] [ Biome.WASTELAND, 2 ] ],
[Biome.BADLANDS]: [ Biome.DESERT, Biome.MOUNTAIN ],
[Biome.CAVE]: [ Biome.BADLANDS, Biome.LAKE ],
[Biome.DESERT]: Biome.RUINS,
[Biome.DESERT]: [ Biome.RUINS, [ Biome.CONSTRUCTION_SITE, 2 ] ],
[Biome.ICE_CAVE]: Biome.SNOWY_FOREST,
[Biome.MEADOW]: [ Biome.PLAINS, [ Biome.FAIRY_CAVE, 2 ] ],
[Biome.MEADOW]: [ Biome.PLAINS, Biome.FAIRY_CAVE ],
[Biome.POWER_PLANT]: Biome.FACTORY,
[Biome.VOLCANO]: [ Biome.BEACH, [ Biome.ICE_CAVE, 4 ] ],
[Biome.VOLCANO]: [ Biome.BEACH, [ Biome.ICE_CAVE, 3 ] ],
[Biome.GRAVEYARD]: Biome.ABYSS,
[Biome.DOJO]: [ Biome.PLAINS, [ Biome.TEMPLE, 3 ] ],
[Biome.FACTORY]: [ Biome.PLAINS, [ Biome.LABORATORY, 4 ] ],
[Biome.DOJO]: [ Biome.PLAINS, [ Biome.JUNGLE, 2], [ Biome.TEMPLE, 2 ] ],
[Biome.FACTORY]: [ Biome.TALL_GRASS, [ Biome.LABORATORY, 3 ] ],
[Biome.RUINS]: [ Biome.FOREST ],
[Biome.WASTELAND]: Biome.BADLANDS,
[Biome.ABYSS]: [ Biome.CAVE, [ Biome.SPACE, 3 ], [ Biome.WASTELAND, 3 ] ],
[Biome.SPACE]: Biome.RUINS,
[Biome.CONSTRUCTION_SITE]: [ Biome.DOJO, Biome.POWER_PLANT ],
[Biome.CONSTRUCTION_SITE]: [ Biome.POWER_PLANT, [ Biome.DOJO, 2 ] ],
[Biome.JUNGLE]: [ Biome.TEMPLE ],
[Biome.FAIRY_CAVE]: [ Biome.ICE_CAVE, [ Biome.SPACE, 3 ] ],
[Biome.TEMPLE]: [ Biome.SWAMP, [ Biome.RUINS, 3 ] ],
[Biome.FAIRY_CAVE]: [ Biome.ICE_CAVE, [ Biome.SPACE, 2 ] ],
[Biome.TEMPLE]: [ Biome.DESERT, [ Biome.SWAMP, 2 ], [ Biome.RUINS, 2 ] ],
[Biome.METROPOLIS]: Biome.SLUM,
[Biome.SNOWY_FOREST]: [ Biome.FOREST, Biome.LAKE, Biome.MOUNTAIN ],
[Biome.SNOWY_FOREST]: [ Biome.FOREST, Biome.MOUNTAIN, [ Biome.LAKE, 2 ] ],
[Biome.ISLAND]: Biome.SEA,
[Biome.LABORATORY]: Biome.CONSTRUCTION_SITE
};
@ -7663,6 +7663,12 @@ export function initBiomes() {
biomeDepths[Biome.TOWN] = [ 0, 1 ];
const traverseBiome = (biome: Biome, depth: integer) => {
if (biome === Biome.END) {
const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key)));
biomeList.pop(); // Removes Biome.END from the list
const randIndex = Utils.randInt(biomeList.length, 2); // Will never be Biome.TOWN or Biome.PLAINS
biome = Biome[biomeList[randIndex]];
}
const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome])
? biomeLinks[biome] as (Biome | [ Biome, integer ])[]
: [ biomeLinks[biome] as Biome ];

View File

@ -55,6 +55,11 @@ export enum ChallengeType {
* @see {@link Challenge.applyFixedBattle}
*/
FIXED_BATTLES,
/**
* Modifies the effectiveness of Type matchups in battle
* @see {@linkcode Challenge.applyTypeEffectiveness}
*/
TYPE_EFFECTIVENESS,
/**
* Modifies what level the AI pokemon are. UNIMPLEMENTED.
*/
@ -327,6 +332,15 @@ export abstract class Challenge {
return false;
}
/**
* An apply function for TYPE_EFFECTIVENESS challenges. Derived classes should alter this.
* @param effectiveness {@linkcode Utils.NumberHolder} The current effectiveness of the move.
* @returns Whether this function did anything.
*/
applyTypeEffectiveness(effectiveness: Utils.NumberHolder): boolean {
return false;
}
/**
* An apply function for AI_LEVEL challenges. Derived classes should alter this.
* @param level {@link Utils.IntegerHolder} The generated level.
@ -651,10 +665,7 @@ export class FreshStartChallenge extends Challenge {
return true;
}
/**
* @overrides
*/
getDifficulty(): number {
override getDifficulty(): number {
return 0;
}
@ -666,6 +677,38 @@ export class FreshStartChallenge extends Challenge {
}
}
/**
* Implements an inverse battle challenge.
*/
export class InverseBattleChallenge extends Challenge {
constructor() {
super(Challenges.INVERSE_BATTLE, 1);
}
static loadChallenge(source: InverseBattleChallenge | any): InverseBattleChallenge {
const newChallenge = new InverseBattleChallenge();
newChallenge.value = source.value;
newChallenge.severity = source.severity;
return newChallenge;
}
override getDifficulty(): number {
return 0;
}
applyTypeEffectiveness(effectiveness: Utils.NumberHolder): boolean {
if (effectiveness.value < 1) {
effectiveness.value = 2;
return true;
} else if (effectiveness.value > 1) {
effectiveness.value = 0.5;
return true;
}
return false;
}
}
/**
* Lowers the amount of starter points available.
*/
@ -785,6 +828,14 @@ export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType
* @returns True if any challenge was successfully applied.
*/
export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.FIXED_BATTLES, waveIndex: Number, battleConfig: FixedBattleConfig): boolean;
/**
* Apply all challenges that modify type effectiveness.
* @param gameMode {@linkcode GameMode} The current gameMode
* @param challengeType {@linkcode ChallengeType} ChallengeType.TYPE_EFFECTIVENESS
* @param effectiveness {@linkcode Utils.NumberHolder} The current effectiveness of the move.
* @returns True if any challenge was successfully applied.
*/
export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.TYPE_EFFECTIVENESS, effectiveness: Utils.NumberHolder): boolean;
/**
* Apply all challenges that modify what level AI are.
* @param gameMode {@link GameMode} The current gameMode
@ -866,6 +917,9 @@ export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType
case ChallengeType.FIXED_BATTLES:
ret ||= c.applyFixedBattle(args[0], args[1]);
break;
case ChallengeType.TYPE_EFFECTIVENESS:
ret ||= c.applyTypeEffectiveness(args[0]);
break;
case ChallengeType.AI_LEVEL:
ret ||= c.applyLevelChange(args[0], args[1], args[2], args[3]);
break;
@ -907,6 +961,8 @@ export function copyChallenge(source: Challenge | any): Challenge {
return LowerStarterPointsChallenge.loadChallenge(source);
case Challenges.FRESH_START:
return FreshStartChallenge.loadChallenge(source);
case Challenges.INVERSE_BATTLE:
return InverseBattleChallenge.loadChallenge(source);
}
throw new Error("Unknown challenge copied");
}
@ -918,5 +974,6 @@ export function initChallenges() {
new SingleGenerationChallenge(),
new SingleTypeChallenge(),
new FreshStartChallenge(),
new InverseBattleChallenge(),
);
}

View File

@ -43,7 +43,7 @@ export const speciesEggMoves = {
[Species.SHELLDER]: [ Moves.ROCK_BLAST, Moves.WATER_SHURIKEN, Moves.BANEFUL_BUNKER, Moves.BONE_RUSH ],
[Species.GASTLY]: [ Moves.SLUDGE_BOMB, Moves.AURA_SPHERE, Moves.NASTY_PLOT, Moves.ASTRAL_BARRAGE ],
[Species.ONIX]: [ Moves.SHORE_UP, Moves.BODY_PRESS, Moves.HEAVY_SLAM, Moves.DIAMOND_STORM ],
[Species.DROWZEE]: [ Moves.BADDY_BAD, Moves.STRENGTH_SAP, Moves.LUMINA_CRASH, Moves.SPORE ],
[Species.DROWZEE]: [ Moves.BADDY_BAD, Moves.STRENGTH_SAP, Moves.LUMINA_CRASH, Moves.DARK_VOID ],
[Species.KRABBY]: [ Moves.FIRE_LASH, Moves.PLAY_ROUGH, Moves.IVY_CUDGEL, Moves.SHELL_SMASH ],
[Species.VOLTORB]: [ Moves.NASTY_PLOT, Moves.OVERHEAT, Moves.FROST_BREATH, Moves.ELECTRO_DRIFT ],
[Species.EXEGGCUTE]: [ Moves.FICKLE_BEAM, Moves.APPLE_ACID, Moves.TRICK_ROOM, Moves.LUMINA_CRASH ],
@ -125,7 +125,7 @@ export const speciesEggMoves = {
[Species.SUICUNE]: [ Moves.RECOVER, Moves.NASTY_PLOT, Moves.FREEZE_DRY, Moves.STEAM_ERUPTION ],
[Species.LARVITAR]: [ Moves.DRAGON_DANCE, Moves.MOUNTAIN_GALE, Moves.SHORE_UP, Moves.DIAMOND_STORM ],
[Species.LUGIA]: [ Moves.NASTY_PLOT, Moves.LUMINA_CRASH, Moves.AURA_SPHERE, Moves.OBLIVION_WING ],
[Species.HO_OH]: [ Moves.FLOATY_FALL, Moves.SOLAR_BLADE, Moves.REVIVAL_BLESSING, Moves.BOLT_BEAK ],
[Species.HO_OH]: [ Moves.FLOATY_FALL, Moves.PRECIPICE_BLADES, Moves.REVIVAL_BLESSING, Moves.BOLT_BEAK ],
[Species.CELEBI]: [ Moves.PHOTON_GEYSER, Moves.MATCHA_GOTCHA, Moves.REVIVAL_BLESSING, Moves.QUIVER_DANCE ],
[Species.TREECKO]: [ Moves.NASTY_PLOT, Moves.APPLE_ACID, Moves.SECRET_SWORD, Moves.DRAGON_ENERGY ],
[Species.TORCHIC]: [ Moves.HIGH_JUMP_KICK, Moves.SUPERCELL_SLAM, Moves.KNOCK_OFF, Moves.V_CREATE ],
@ -249,7 +249,7 @@ export const speciesEggMoves = {
[Species.CRESSELIA]: [ Moves.COSMIC_POWER, Moves.SECRET_SWORD, Moves.SIZZLY_SLIDE, Moves.LUMINA_CRASH ],
[Species.PHIONE]: [ Moves.BOUNCY_BUBBLE, Moves.FREEZE_DRY, Moves.SPLISHY_SPLASH, Moves.QUIVER_DANCE ],
[Species.MANAPHY]: [ Moves.BOUNCY_BUBBLE, Moves.FREEZE_DRY, Moves.SPLISHY_SPLASH, Moves.QUIVER_DANCE ],
[Species.DARKRAI]: [ Moves.FIERY_WRATH, Moves.MOONBLAST, Moves.SEARING_SHOT, Moves.SPORE ],
[Species.DARKRAI]: [ Moves.FIERY_WRATH, Moves.MOONBLAST, Moves.FIERY_DANCE, Moves.MAKE_IT_RAIN ],
[Species.SHAYMIN]: [ Moves.MATCHA_GOTCHA, Moves.FIERY_DANCE, Moves.AEROBLAST, Moves.QUIVER_DANCE ],
[Species.ARCEUS]: [ Moves.NO_RETREAT, Moves.COLLISION_COURSE, Moves.ASTRAL_BARRAGE, Moves.MULTI_ATTACK ],
[Species.VICTINI]: [ Moves.BLUE_FLARE, Moves.BOLT_STRIKE, Moves.LUSTER_PURGE, Moves.VICTORY_DANCE ],

View File

@ -4,7 +4,7 @@ import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTr
import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon";
import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects } from "./status-effect";
import { getTypeResistances, Type } from "./type";
import { getTypeDamageMultiplier, Type } from "./type";
import { Constructor } from "#app/utils";
import * as Utils from "../utils";
import { WeatherType } from "./weather";
@ -35,8 +35,11 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { StatChangePhase } from "#app/phases/stat-change-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js";
import { NumberHolder } from "#app/utils";
import { GameMode } from "#app/game-mode";
import { applyChallenges, ChallengeType } from "./challenge";
export enum MoveCategory {
PHYSICAL,
@ -4194,8 +4197,12 @@ export class WaterSuperEffectTypeMultiplierAttr extends VariableMoveTypeMultipli
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const multiplier = args[0] as Utils.NumberHolder;
if (target.isOfType(Type.WATER)) {
multiplier.value *= 4; // Increased twice because initial reduction against water
return true;
const effectivenessAgainstWater = new Utils.NumberHolder(getTypeDamageMultiplier(move.type, Type.WATER));
applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstWater);
if (effectivenessAgainstWater.value !== 0) {
multiplier.value *= 2 / effectivenessAgainstWater.value;
return true;
}
}
return false;
@ -6217,7 +6224,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
return false;
}
const userTypes = user.getTypes();
const validTypes = getTypeResistances(moveData.type).filter(t => !userTypes.includes(t)); // valid types are ones that are not already the user's types
const validTypes = this.getTypeResistances(user.scene.gameMode, moveData.type).filter(t => !userTypes.includes(t)); // valid types are ones that are not already the user's types
if (!validTypes.length) {
return false;
}
@ -6229,6 +6236,25 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
return true;
}
/**
* Retrieve the types resisting a given type. Used by Conversion 2
* @returns An array populated with Types, or an empty array if no resistances exist (Unknown or Stellar type)
*/
getTypeResistances(gameMode: GameMode, type: number): Type[] {
const typeResistances: Type[] = [];
for (let i = 0; i < Object.keys(Type).length; i++) {
const multiplier = new NumberHolder(1);
multiplier.value = getTypeDamageMultiplier(type, i);
applyChallenges(gameMode, ChallengeType.TYPE_EFFECTIVENESS, multiplier);
if (multiplier.value < 1) {
typeResistances.push(i);
}
}
return typeResistances;
}
getCondition(): MoveConditionFunc {
return (user, target, move) => {
const moveHistory = target.getLastXMoves();
@ -7954,7 +7980,8 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6)
.attr(StatusEffectAttr, StatusEffect.FREEZE)
.attr(WaterSuperEffectTypeMultiplierAttr),
.attr(WaterSuperEffectTypeMultiplierAttr)
.partial(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell.
new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6)
.soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES),

View File

@ -1132,7 +1132,7 @@ export function initSpecies() {
),
new PokemonSpecies(Species.SNORLAX, 1, false, false, false, "Sleeping Pokémon", Type.NORMAL, null, 2.1, 460, Abilities.IMMUNITY, Abilities.THICK_FAT, Abilities.GLUTTONY, 540, 160, 110, 65, 65, 110, 30, 25, 50, 189, GrowthRate.SLOW, 87.5, false, true,
new PokemonForm("Normal", "", Type.NORMAL, null, 2.1, 460, Abilities.IMMUNITY, Abilities.THICK_FAT, Abilities.GLUTTONY, 540, 160, 110, 65, 65, 110, 30, 25, 50, 189, false, null, true),
new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, Type.GRASS, 35, 460, Abilities.THICK_FAT, Abilities.THICK_FAT, Abilities.THICK_FAT, 640, 200, 135, 85, 80, 125, 15, 25, 50, 189),
new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 35, 460, Abilities.THICK_FAT, Abilities.THICK_FAT, Abilities.THICK_FAT, 640, 200, 135, 85, 80, 125, 15, 25, 50, 189),
),
new PokemonSpecies(Species.ARTICUNO, 1, true, false, false, "Freeze Pokémon", Type.ICE, Type.FLYING, 1.7, 55.4, Abilities.PRESSURE, Abilities.NONE, Abilities.SNOW_CLOAK, 580, 90, 85, 100, 95, 125, 85, 3, 35, 290, GrowthRate.SLOW, null, false),
new PokemonSpecies(Species.ZAPDOS, 1, true, false, false, "Electric Pokémon", Type.ELECTRIC, Type.FLYING, 1.6, 52.6, Abilities.PRESSURE, Abilities.NONE, Abilities.STATIC, 580, 90, 90, 85, 125, 90, 100, 3, 35, 290, GrowthRate.SLOW, null, false),
@ -2252,19 +2252,19 @@ export function initSpecies() {
new PokemonSpecies(Species.THWACKEY, 8, false, false, false, "Beat Pokémon", Type.GRASS, null, 0.7, 14, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 420, 70, 85, 70, 55, 60, 80, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false),
new PokemonSpecies(Species.RILLABOOM, 8, false, false, false, "Drummer Pokémon", Type.GRASS, null, 2.1, 90, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 530, 100, 125, 90, 60, 70, 85, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true,
new PokemonForm("Normal", "", Type.GRASS, null, 2.1, 90, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 530, 100, 125, 90, 60, 70, 85, 45, 50, 265, false, null, true),
new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, null, 28, 90, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, 630, 125, 150, 115, 65, 95, 80, 45, 50, 265),
new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, null, 28, 90, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, 630, 125, 150, 105, 85, 85, 80, 45, 50, 265),
),
new PokemonSpecies(Species.SCORBUNNY, 8, false, false, false, "Rabbit Pokémon", Type.FIRE, null, 0.3, 4.5, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 310, 50, 71, 40, 40, 40, 69, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false),
new PokemonSpecies(Species.RABOOT, 8, false, false, false, "Rabbit Pokémon", Type.FIRE, null, 0.6, 9, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 420, 65, 86, 60, 55, 60, 94, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false),
new PokemonSpecies(Species.CINDERACE, 8, false, false, false, "Striker Pokémon", Type.FIRE, null, 1.4, 33, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 530, 80, 116, 75, 65, 75, 119, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true,
new PokemonForm("Normal", "", Type.FIRE, null, 1.4, 33, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 530, 80, 116, 75, 65, 75, 119, 45, 50, 265, false, null, true),
new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, null, 27, 33, Abilities.LIBERO, Abilities.LIBERO, Abilities.LIBERO, 630, 90, 151, 85, 85, 85, 134, 45, 50, 265),
new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, null, 27, 33, Abilities.LIBERO, Abilities.LIBERO, Abilities.LIBERO, 630, 100, 146, 80, 90, 80, 134, 45, 50, 265),
),
new PokemonSpecies(Species.SOBBLE, 8, false, false, false, "Water Lizard Pokémon", Type.WATER, null, 0.3, 4, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 310, 50, 40, 40, 70, 40, 70, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false),
new PokemonSpecies(Species.DRIZZILE, 8, false, false, false, "Water Lizard Pokémon", Type.WATER, null, 0.7, 11.5, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 420, 65, 60, 55, 95, 55, 90, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false),
new PokemonSpecies(Species.INTELEON, 8, false, false, false, "Secret Agent Pokémon", Type.WATER, null, 1.9, 45.2, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 530, 70, 85, 65, 125, 65, 120, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true,
new PokemonForm("Normal", "", Type.WATER, null, 1.9, 45.2, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 530, 70, 85, 65, 125, 65, 120, 45, 50, 265, false, null, true),
new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 40, 45.2, Abilities.SNIPER, Abilities.SNIPER, Abilities.SNIPER, 630, 90, 90, 85, 150, 85, 130, 45, 50, 265),
new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 40, 45.2, Abilities.SNIPER, Abilities.SNIPER, Abilities.SNIPER, 630, 95, 97, 77, 147, 77, 137, 45, 50, 265),
),
new PokemonSpecies(Species.SKWOVET, 8, false, false, false, "Cheeky Pokémon", Type.NORMAL, null, 0.3, 2.5, Abilities.CHEEK_POUCH, Abilities.NONE, Abilities.GLUTTONY, 275, 70, 55, 55, 35, 35, 25, 255, 50, 55, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.GREEDENT, 8, false, false, false, "Greedy Pokémon", Type.NORMAL, null, 0.6, 6, Abilities.CHEEK_POUCH, Abilities.NONE, Abilities.GLUTTONY, 460, 120, 95, 95, 55, 75, 20, 90, 50, 161, GrowthRate.MEDIUM_FAST, 50, false),
@ -3470,7 +3470,7 @@ export const starterPassiveAbilities = {
[Species.SUICUNE]: Abilities.UNAWARE,
[Species.LARVITAR]: Abilities.SAND_RUSH,
[Species.LUGIA]: Abilities.DELTA_STREAM,
[Species.HO_OH]: Abilities.DROUGHT,
[Species.HO_OH]: Abilities.MAGIC_GUARD,
[Species.CELEBI]: Abilities.GRASSY_SURGE,
[Species.TREECKO]: Abilities.TINTED_LENS,
[Species.TORCHIC]: Abilities.RECKLESS,
@ -3591,7 +3591,7 @@ export const starterPassiveAbilities = {
[Species.HEATRAN]: Abilities.EARTH_EATER,
[Species.REGIGIGAS]: Abilities.MINDS_EYE,
[Species.GIRATINA]: Abilities.SHADOW_SHIELD,
[Species.CRESSELIA]: Abilities.UNAWARE,
[Species.CRESSELIA]: Abilities.SHADOW_SHIELD,
[Species.PHIONE]: Abilities.SIMPLE,
[Species.MANAPHY]: Abilities.PRIMORDIAL_SEA,
[Species.DARKRAI]: Abilities.UNNERVE,

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ export enum Type {
export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8;
export function getTypeDamageMultiplier(attackType: integer, defType: integer): TypeDamageMultiplier {
export function getTypeDamageMultiplier(attackType: Type, defType: Type): TypeDamageMultiplier {
if (attackType === Type.UNKNOWN || defType === Type.UNKNOWN) {
return 1;
}
@ -33,26 +33,10 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
switch (attackType) {
case Type.FIGHTING:
return 2;
case Type.NORMAL:
case Type.FLYING:
case Type.POISON:
case Type.GROUND:
case Type.ROCK:
case Type.BUG:
case Type.STEEL:
case Type.FIRE:
case Type.WATER:
case Type.GRASS:
case Type.ELECTRIC:
case Type.PSYCHIC:
case Type.ICE:
case Type.DRAGON:
case Type.DARK:
case Type.FAIRY:
return 1;
case Type.GHOST:
default:
return 0;
default:
return 1;
}
case Type.FIGHTING:
switch (attackType) {
@ -60,25 +44,12 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.PSYCHIC:
case Type.FAIRY:
return 2;
case Type.NORMAL:
case Type.FIGHTING:
case Type.POISON:
case Type.GROUND:
case Type.GHOST:
case Type.STEEL:
case Type.FIRE:
case Type.WATER:
case Type.GRASS:
case Type.ELECTRIC:
case Type.ICE:
case Type.DRAGON:
return 1;
case Type.ROCK:
case Type.BUG:
case Type.DARK:
return 0.5;
default:
return 0;
return 1;
}
case Type.FLYING:
switch (attackType) {
@ -86,43 +57,20 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.ELECTRIC:
case Type.ICE:
return 2;
case Type.NORMAL:
case Type.FLYING:
case Type.POISON:
case Type.GHOST:
case Type.STEEL:
case Type.FIRE:
case Type.WATER:
case Type.PSYCHIC:
case Type.DRAGON:
case Type.DARK:
case Type.FAIRY:
return 1;
case Type.FIGHTING:
case Type.BUG:
case Type.GRASS:
return 0.5;
case Type.GROUND:
default:
return 0;
default:
return 1;
}
case Type.POISON:
switch (attackType) {
case Type.GROUND:
case Type.PSYCHIC:
return 2;
case Type.NORMAL:
case Type.FLYING:
case Type.ROCK:
case Type.GHOST:
case Type.STEEL:
case Type.FIRE:
case Type.WATER:
case Type.ELECTRIC:
case Type.ICE:
case Type.DRAGON:
case Type.DARK:
return 1;
case Type.FIGHTING:
case Type.POISON:
case Type.BUG:
@ -130,7 +78,7 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.FAIRY:
return 0.5;
default:
return 0;
return 1;
}
case Type.GROUND:
switch (attackType) {
@ -138,25 +86,13 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.GRASS:
case Type.ICE:
return 2;
case Type.NORMAL:
case Type.FIGHTING:
case Type.FLYING:
case Type.GROUND:
case Type.BUG:
case Type.GHOST:
case Type.STEEL:
case Type.FIRE:
case Type.PSYCHIC:
case Type.DRAGON:
case Type.DARK:
case Type.FAIRY:
return 1;
case Type.POISON:
case Type.ROCK:
return 0.5;
case Type.ELECTRIC:
default:
return 0;
default:
return 1;
}
case Type.ROCK:
switch (attackType) {
@ -166,23 +102,13 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.WATER:
case Type.GRASS:
return 2;
case Type.ROCK:
case Type.BUG:
case Type.GHOST:
case Type.ELECTRIC:
case Type.PSYCHIC:
case Type.ICE:
case Type.DRAGON:
case Type.DARK:
case Type.FAIRY:
return 1;
case Type.NORMAL:
case Type.FLYING:
case Type.POISON:
case Type.FIRE:
return 0.5;
default:
return 0;
return 1;
}
case Type.BUG:
switch (attackType) {
@ -190,51 +116,26 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.ROCK:
case Type.FIRE:
return 2;
case Type.NORMAL:
case Type.POISON:
case Type.BUG:
case Type.GHOST:
case Type.STEEL:
case Type.WATER:
case Type.ELECTRIC:
case Type.PSYCHIC:
case Type.ICE:
case Type.DRAGON:
case Type.DARK:
case Type.FAIRY:
return 1;
case Type.FIGHTING:
case Type.GROUND:
case Type.GRASS:
return 0.5;
default:
return 0;
return 1;
}
case Type.GHOST:
switch (attackType) {
case Type.GHOST:
case Type.DARK:
return 2;
case Type.FLYING:
case Type.GROUND:
case Type.ROCK:
case Type.STEEL:
case Type.FIRE:
case Type.WATER:
case Type.GRASS:
case Type.ELECTRIC:
case Type.PSYCHIC:
case Type.ICE:
case Type.DRAGON:
case Type.FAIRY:
return 1;
case Type.POISON:
case Type.BUG:
return 0.5;
case Type.NORMAL:
case Type.FIGHTING:
default:
return 0;
default:
return 1;
}
case Type.STEEL:
switch (attackType) {
@ -242,11 +143,6 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.GROUND:
case Type.FIRE:
return 2;
case Type.GHOST:
case Type.WATER:
case Type.ELECTRIC:
case Type.DARK:
return 1;
case Type.NORMAL:
case Type.FLYING:
case Type.ROCK:
@ -259,8 +155,9 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.FAIRY:
return 0.5;
case Type.POISON:
default:
return 0;
default:
return 1;
}
case Type.FIRE:
switch (attackType) {
@ -268,16 +165,6 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.ROCK:
case Type.WATER:
return 2;
case Type.NORMAL:
case Type.FIGHTING:
case Type.FLYING:
case Type.POISON:
case Type.GHOST:
case Type.ELECTRIC:
case Type.PSYCHIC:
case Type.DRAGON:
case Type.DARK:
return 1;
case Type.BUG:
case Type.STEEL:
case Type.FIRE:
@ -286,33 +173,20 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.FAIRY:
return 0.5;
default:
return 0;
return 1;
}
case Type.WATER:
switch (attackType) {
case Type.GRASS:
case Type.ELECTRIC:
return 2;
case Type.NORMAL:
case Type.FIGHTING:
case Type.FLYING:
case Type.POISON:
case Type.GROUND:
case Type.ROCK:
case Type.BUG:
case Type.GHOST:
case Type.PSYCHIC:
case Type.DRAGON:
case Type.DARK:
case Type.FAIRY:
return 1;
case Type.STEEL:
case Type.FIRE:
case Type.WATER:
case Type.ICE:
return 0.5;
default:
return 0;
return 1;
}
case Type.GRASS:
switch (attackType) {
@ -322,49 +196,24 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.FIRE:
case Type.ICE:
return 2;
case Type.NORMAL:
case Type.FIGHTING:
case Type.ROCK:
case Type.GHOST:
case Type.STEEL:
case Type.PSYCHIC:
case Type.DRAGON:
case Type.DARK:
case Type.FAIRY:
return 1;
case Type.GROUND:
case Type.WATER:
case Type.GRASS:
case Type.ELECTRIC:
return 0.5;
default:
return 0;
return 1;
}
case Type.ELECTRIC:
switch (attackType) {
case Type.GROUND:
return 2;
case Type.NORMAL:
case Type.FIGHTING:
case Type.POISON:
case Type.ROCK:
case Type.BUG:
case Type.GHOST:
case Type.FIRE:
case Type.WATER:
case Type.GRASS:
case Type.PSYCHIC:
case Type.ICE:
case Type.DRAGON:
case Type.DARK:
case Type.FAIRY:
return 1;
case Type.FLYING:
case Type.STEEL:
case Type.ELECTRIC:
return 0.5;
default:
return 0;
return 1;
}
case Type.PSYCHIC:
switch (attackType) {
@ -372,25 +221,11 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.GHOST:
case Type.DARK:
return 2;
case Type.NORMAL:
case Type.FLYING:
case Type.POISON:
case Type.GROUND:
case Type.ROCK:
case Type.STEEL:
case Type.FIRE:
case Type.WATER:
case Type.GRASS:
case Type.ELECTRIC:
case Type.ICE:
case Type.DRAGON:
case Type.FAIRY:
return 1;
case Type.FIGHTING:
case Type.PSYCHIC:
return 0.5;
default:
return 0;
return 1;
}
case Type.ICE:
switch (attackType) {
@ -399,24 +234,10 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.STEEL:
case Type.FIRE:
return 2;
case Type.NORMAL:
case Type.FLYING:
case Type.POISON:
case Type.GROUND:
case Type.BUG:
case Type.GHOST:
case Type.WATER:
case Type.GRASS:
case Type.ELECTRIC:
case Type.PSYCHIC:
case Type.DRAGON:
case Type.DARK:
case Type.FAIRY:
return 1;
case Type.ICE:
return 0.5;
default:
return 0;
return 1;
}
case Type.DRAGON:
switch (attackType) {
@ -424,25 +245,13 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.DRAGON:
case Type.FAIRY:
return 2;
case Type.NORMAL:
case Type.FIGHTING:
case Type.FLYING:
case Type.POISON:
case Type.GROUND:
case Type.ROCK:
case Type.BUG:
case Type.GHOST:
case Type.STEEL:
case Type.PSYCHIC:
case Type.DARK:
return 1;
case Type.FIRE:
case Type.WATER:
case Type.GRASS:
case Type.ELECTRIC:
return 0.5;
default:
return 0;
return 1;
}
case Type.DARK:
switch (attackType) {
@ -450,106 +259,33 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer):
case Type.BUG:
case Type.FAIRY:
return 2;
case Type.NORMAL:
case Type.FLYING:
case Type.POISON:
case Type.GROUND:
case Type.ROCK:
case Type.STEEL:
case Type.FIRE:
case Type.WATER:
case Type.GRASS:
case Type.ELECTRIC:
case Type.ICE:
case Type.DRAGON:
return 1;
case Type.GHOST:
case Type.DARK:
return 0.5;
case Type.PSYCHIC:
default:
return 0;
default:
return 1;
}
case Type.FAIRY:
switch (attackType) {
case Type.POISON:
case Type.STEEL:
return 2;
case Type.NORMAL:
case Type.FLYING:
case Type.GROUND:
case Type.ROCK:
case Type.GHOST:
case Type.FIRE:
case Type.WATER:
case Type.GRASS:
case Type.ELECTRIC:
case Type.PSYCHIC:
case Type.ICE:
case Type.FAIRY:
return 1;
case Type.FIGHTING:
case Type.BUG:
case Type.DARK:
return 0.5;
case Type.DRAGON:
default:
return 0;
default:
return 1;
}
case Type.STELLAR:
return 1;
}
return 0;
}
/**
* Retrieve the types resisting a given type
* @returns An array populated with Types, or an empty array if no resistances exist (Unknown or Stellar type)
*/
export function getTypeResistances(type: number): Type[] {
switch (type) {
case Type.NORMAL:
return [Type.ROCK, Type.STEEL, Type.GHOST];
case Type.FIGHTING:
return [Type.FLYING, Type.POISON, Type.BUG, Type.PSYCHIC, Type.FAIRY, Type.GHOST];
case Type.FLYING:
return [Type.ROCK, Type.ELECTRIC, Type.STEEL];
case Type.POISON:
return [Type.POISON, Type.GROUND, Type.ROCK, Type.GHOST, Type.STEEL];
case Type.GROUND:
return [Type.BUG, Type.GRASS, Type.FLYING];
case Type.ROCK:
return [Type.FIGHTING, Type.GROUND, Type.STEEL];
case Type.BUG:
return [Type.FIGHTING, Type.FLYING, Type.POISON, Type.GHOST, Type.STEEL, Type.FIRE, Type.FAIRY];
case Type.GHOST:
return [Type.DARK, Type.NORMAL];
case Type.STEEL:
return [Type.STEEL, Type.FIRE, Type.WATER, Type.ELECTRIC];
case Type.FIRE:
return [Type.ROCK, Type.FIRE, Type.WATER, Type.DRAGON];
case Type.WATER:
return [Type.WATER, Type.GRASS, Type.DRAGON];
case Type.GRASS:
return [Type.FLYING, Type.POISON, Type.BUG, Type.STEEL, Type.FIRE, Type.GRASS, Type.DRAGON];
case Type.ELECTRIC:
return [Type.GRASS, Type.ELECTRIC, Type.DRAGON, Type.GROUND];
case Type.PSYCHIC:
return [Type.STEEL, Type.PSYCHIC];
case Type.ICE:
return [Type.STEEL, Type.FIRE, Type.WATER, Type.ICE];
case Type.DRAGON:
return [Type.STEEL, Type.FAIRY];
case Type.DARK:
return [Type.FIGHTING, Type.DARK, Type.FAIRY];
case Type.FAIRY:
return [Type.POISON, Type.STEEL, Type.FIRE];
case Type.UNKNOWN:
case Type.STELLAR:
default:
return [];
}
return 1;
}
/**

View File

@ -3,5 +3,6 @@ export enum Challenges {
SINGLE_TYPE,
LOWER_MAX_STARTER_COST,
LOWER_STARTER_POINTS,
FRESH_START
FRESH_START,
INVERSE_BATTLE,
}

View File

@ -49,6 +49,7 @@ import { BerryType } from "#enums/berry-type";
import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Challenges } from "#enums/challenges";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { DamagePhase } from "#app/phases/damage-phase.js";
import { FaintPhase } from "#app/phases/faint-phase.js";
@ -1315,12 +1316,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return 1;
}
}
return getTypeDamageMultiplier(moveType, defType);
const multiplier = new Utils.NumberHolder(getTypeDamageMultiplier(moveType, defType));
applyChallenges(this.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, multiplier);
return multiplier.value;
}).reduce((acc, cur) => acc * cur, 1) as TypeDamageMultiplier;
const typeMultiplierAgainstFlying = new Utils.NumberHolder(getTypeDamageMultiplier(moveType, Type.FLYING));
applyChallenges(this.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, typeMultiplierAgainstFlying);
// Handle strong winds lowering effectiveness of types super effective against pure flying
if (!ignoreStrongWinds && arena.weather?.weatherType === WeatherType.STRONG_WINDS && !arena.weather.isEffectSuppressed(this.scene) && this.isOfType(Type.FLYING) && getTypeDamageMultiplier(moveType, Type.FLYING) === 2) {
if (!ignoreStrongWinds && arena.weather?.weatherType === WeatherType.STRONG_WINDS && !arena.weather.isEffectSuppressed(this.scene) && this.isOfType(Type.FLYING) && typeMultiplierAgainstFlying.value === 2) {
multiplier /= 2;
if (!simulated) {
this.scene.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
@ -3842,7 +3846,7 @@ export class EnemyPokemon extends Pokemon {
this.moveset = (formIndex !== undefined ? formIndex : this.formIndex)
? [
new PokemonMove(Moves.DYNAMAX_CANNON),
new PokemonMove(Moves.SLUDGE_BOMB),
new PokemonMove(Moves.CROSS_POISON),
new PokemonMove(Moves.FLAMETHROWER),
new PokemonMove(Moves.RECOVER, 0, -4)
]
@ -3852,6 +3856,9 @@ export class EnemyPokemon extends Pokemon {
new PokemonMove(Moves.FLAMETHROWER),
new PokemonMove(Moves.COSMIC_POWER)
];
if (this.scene.gameMode.hasChallenge(Challenges.INVERSE_BATTLE)) {
this.moveset[2] = new PokemonMove(Moves.THUNDERBOLT);
}
break;
default:
super.generateAndPopulateMoveset();

View File

@ -269,5 +269,9 @@
"FRESH_START": {
"name": "Hussa, noch einmal von vorn!",
"description": "Schließe die 'Neuanfang' Herausforderung ab"
},
"INVERSE_BATTLE": {
"name": "Spieglein, Spieglein an der Wand",
"description": "Schließe die 'Umkehrkampf' Herausforderung ab"
}
}

View File

@ -25,5 +25,12 @@
"desc": "Du kannst nur die ursprünglichen Starter verwenden, genau so, als hättest du gerade erst mit Pokérogue begonnen.",
"value.0": "Aus",
"value.1": "An"
},
"inverseBattle": {
"name": "Umkehrkampf",
"shortName": "Umkehrkampf",
"desc": "Die Typen-Effektivität wird umgekehrt, und kein Typ ist gegen einen anderen Typ immun.\nDeaktiviert die Erfolge anderer Herausforderungen.",
"value.0": "Aus",
"value.1": "An"
}
}

View File

@ -260,5 +260,9 @@
"FRESH_START": {
"name": "First Try!",
"description": "Complete the Fresh Start challenge."
},
"INVERSE_BATTLE": {
"name": "Mirror rorriM",
"description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC"
}
}

View File

@ -279,5 +279,9 @@
"FRESH_START": {
"name": "First Try!",
"description": "Complete the Fresh Start challenge."
},
"INVERSE_BATTLE": {
"name": "Mirror rorriM",
"description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC"
}
}

View File

@ -25,5 +25,12 @@
"desc": "You can only use the original starters, and only as if you had just started PokéRogue.",
"value.0": "Off",
"value.1": "On"
},
"inverseBattle": {
"name": "Inverse Battle",
"shortName": "Inverse",
"desc": "Type matchups are reversed and no type is immune to any other type.\nDisables other challenges' achievements.",
"value.0": "Off",
"value.1": "On"
}
}

View File

@ -2,5 +2,9 @@
"encounter": "It appears the time has finally come once again.\nYou know why you have come here, do you not?\n$You were drawn here, because you have been here before.\nCountless times.\n$Though, perhaps it can be counted.\nTo be precise, this is in fact your {{cycleCount}} cycle.\n$Each cycle your mind reverts to its former state.\nEven so, somehow, remnants of your former selves remain.\n$Until now you have yet to succeed, but I sense a different presence in you this time.\n\n$You are the only one here, though it is as if there is… another.\n$Will you finally prove a formidable challenge to me?\nThe challenge I have longed after for millennia?\n$We begin.",
"encounter_female": null,
"firstStageWin": "I see. The presence I felt was indeed real.\nIt appears I no longer need to hold back.\n$Do not disappoint me.",
"secondStageWin": "…Magnificent."
"secondStageWin": "…Magnificent.",
"key_ordinal_one": "st",
"key_ordinal_two": "nd",
"key_ordinal_few": "rd",
"key_ordinal_other": "th"
}

View File

@ -14,8 +14,8 @@
"importSlotSelect": "Select a slot to import to.",
"exportSession": "Export Session",
"exportSlotSelect": "Select a slot to export from.",
"importRunHistory":"Import Run History",
"exportRunHistory":"Export Run History",
"importRunHistory": "Import Run History",
"exportRunHistory": "Export Run History",
"importData": "Import Data",
"exportData": "Export Data",
"consentPreferences": "Consent Preferences",

View File

@ -170,5 +170,9 @@
"CLASSIC_VICTORY": {
"name": "Imbatible",
"description": "Completa el juego en modo clásico."
},
"INVERSE_BATTLE": {
"name": "Espejo ojepsE",
"description": "Completa el reto de Combate Inverso.\n.osrevnI etabmoC ed oter le atelpmoC"
}
}

View File

@ -18,5 +18,12 @@
"name": "Monotipo",
"desc": "Solo puedes usar Pokémon with the {{type}} type.",
"desc_default": "Solo puedes usar Pokémon del tipo elegido."
},
"inverseBattle": {
"name": "Combate Inverso",
"shortName": "Inverso",
"desc": "La efectividad de los tipos es invertida. No hay inmunidades entre tipos.\nEste reto deshabilita logros de otros retos.",
"value.0": "Desactivado",
"value.1": "Activado"
}
}

View File

@ -274,5 +274,9 @@
"FRESH_START": {
"name": "Du premier coup !",
"description": "Terminer un challenge « Nouveau départ »."
},
"INVERSE_BATTLE": {
"name": "La teuté à verlan",
"description": "Terminer un challenge en Combat Inversé.\nMineter un lenjcha en Ba-con Versin."
}
}

View File

@ -25,5 +25,12 @@
"desc": "Vous ne pouvez choisir que les starters de base du jeu, comme si vous le recommenciez.",
"value.0": "Non",
"value.1": "Oui"
},
"inverseBattle": {
"name": "Combat Inversé",
"shortName": "Inversé",
"desc": "Les affinités de la table des types sont inversées et plus aucun type na dimmunité.\nDésactive les succès des autres challenges.",
"value.0": "Non",
"value.1": "Oui"
}
}

View File

@ -22,6 +22,7 @@
},
"freshStart": {
"name": "出直し",
"shortName": "出直し",
"desc": "ポケローグを 始めた ばかりの ような ままで ゲーム開始の 最初のパートナーしか 使えません",
"value.0": "オフ",
"value.1": "オン"

View File

@ -260,5 +260,9 @@
"FRESH_START": {
"name": "첫트!",
"description": "새 출발 챌린지 모드 클리어."
},
"INVERSE_BATTLE": {
"name": "상성 전문가(였던 것)",
"description": "거꾸로 배틀 챌린지 모드 클리어."
}
}

View File

@ -25,5 +25,12 @@
"desc": "포켓로그를 처음 시작했던 때처럼 강화가 전혀 되지 않은 오리지널 스타팅 포켓몬만 고를 수 있습니다.",
"value.0": "해제",
"value.1": "설정"
},
"inverseBattle": {
"name": "거꾸로 배틀",
"shortName": "거꾸로",
"desc": "타입 상성이 반대로 바뀌고 면역 타입은 약점 타입이 됩니다.\n설정 시 다른 챌린지 업적은 달성할 수 없습니다.",
"value.0": "해제",
"value.1": "설정"
}
}

View File

@ -264,5 +264,9 @@
"FRESH_START": {
"name": "De Primeira!",
"description": "Complete o desafio de novo começo."
},
"INVERSE_BATTLE": {
"name": "A torre da derrotA",
"description": "Complete o desafio da Batalha Inversa.\n.asrevnI ahlataB ad oifased o etelpmoC"
}
}

View File

@ -25,5 +25,12 @@
"desc": "Você só pode usar os iniciais originais, como se tivesse acabado de começar o PokéRogue.",
"value.0": "Desligado",
"value.1": "Ligado"
},
"inverseBattle": {
"name": "Batalha Inversa",
"shortName": "Inversa",
"desc": "Fraquezas e resistências de tipos são invertidas e nenhum tipo é imune a outro tipo.\nDesativa as conquistas de outros desafios.",
"value.0": "Desligado",
"value.1": "Ligado"
}
}

View File

@ -268,5 +268,9 @@
"FRESH_START": {
"name": "初次尝试!",
"description": "完成初次尝试挑战"
},
"INVERSE_BATTLE": {
"name": "镜子子镜",
"description": "完成逆转之战挑战\n战挑战之转逆成完"
}
}

View File

@ -25,5 +25,12 @@
"desc": "你只能使用御三家,就像是你第一次玩宝可梦肉鸽一样。",
"value.0": "关闭",
"value.1": "开启"
},
"inverseBattle": {
"name": "逆转之战",
"shortName": "逆转之战",
"desc": "属性相克关系被反转,且没有任何属性对其他属性免疫。\n禁用其他挑战的成就。",
"value.0": "关闭",
"value.1": "开启"
}
}

View File

@ -252,5 +252,9 @@
},
"MONO_FAIRY": {
"name": "林克,醒醒!"
},
"INVERSE_BATTLE": {
"name": "鏡子子鏡",
"description": "完成逆轉之戰挑戰\n戰挑戰之轉逆成完"
}
}

View File

@ -19,5 +19,12 @@
"name": "單屬性",
"desc": "你只能使用{{type}}\n屬性的寶可夢",
"desc_default": "你只能使用所選\n屬性的寶可夢"
},
"inverseBattle": {
"name": "逆轉之戰",
"shortName": "逆轉之戰",
"desc": "屬性相克關系被反轉,且沒有任何屬性對其他屬性免疫。\n禁用其他挑戰的成就。",
"value.0": "關閉",
"value.1": "開啓"
}
}

View File

@ -791,10 +791,10 @@ export class EvolutionItemModifierType extends PokemonModifierType implements Ge
super("", EvolutionItem[evolutionItem].toLowerCase(), (_type, args) => new Modifiers.EvolutionItemModifier(this, (args[0] as PlayerPokemon).id),
(pokemon: PlayerPokemon) => {
if (pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) && pokemonEvolutions[pokemon.species.speciesId].filter(e => e.item === this.evolutionItem
&& (!e.condition || e.condition.predicate(pokemon)) && (e.preFormKey === pokemon.getFormKey())).length && (pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX)) {
&& (!e.condition || e.condition.predicate(pokemon)) && (e.preFormKey === null || e.preFormKey === pokemon.getFormKey())).length && (pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX)) {
return null;
} else if (pokemon.isFusion() && pokemon.fusionSpecies && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) && pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter(e => e.item === this.evolutionItem
&& (!e.condition || e.condition.predicate(pokemon)) && (e.preFormKey === pokemon.getFusionFormKey())).length && (pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX)) {
&& (!e.condition || e.condition.predicate(pokemon)) && (e.preFormKey === null || e.preFormKey === pokemon.getFusionFormKey())).length && (pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX)) {
return null;
}

View File

@ -1,23 +1,23 @@
import BattleScene from "#app/battle-scene.js";
import { BattleType, BattlerIndex } from "#app/battle.js";
import { applyAbAttrs, SyncEncounterNatureAbAttr } from "#app/data/ability.js";
import { getCharVariantFromDialogue } from "#app/data/dialogue.js";
import { TrainerSlot } from "#app/data/trainer-config.js";
import { getRandomWeatherType } from "#app/data/weather.js";
import { BattleSpec } from "#app/enums/battle-spec.js";
import { PlayerGender } from "#app/enums/player-gender.js";
import { Species } from "#app/enums/species.js";
import { EncounterPhaseEvent } from "#app/events/battle-scene.js";
import Pokemon, { FieldPosition } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { regenerateModifierPoolThresholds, ModifierPoolType } from "#app/modifier/modifier-type.js";
import { IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier.js";
import { achvs } from "#app/system/achv.js";
import { handleTutorial, Tutorial } from "#app/tutorial.js";
import { Mode } from "#app/ui/ui.js";
import BattleScene from "#app/battle-scene";
import { BattleType, BattlerIndex } from "#app/battle";
import { applyAbAttrs, SyncEncounterNatureAbAttr } from "#app/data/ability";
import { getCharVariantFromDialogue } from "#app/data/dialogue";
import { TrainerSlot } from "#app/data/trainer-config";
import { getRandomWeatherType } from "#app/data/weather";
import { BattleSpec } from "#app/enums/battle-spec";
import { PlayerGender } from "#app/enums/player-gender";
import { Species } from "#app/enums/species";
import { EncounterPhaseEvent } from "#app/events/battle-scene";
import Pokemon, { FieldPosition } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { regenerateModifierPoolThresholds, ModifierPoolType } from "#app/modifier/modifier-type";
import { IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv";
import { handleTutorial, Tutorial } from "#app/tutorial";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import { BattlePhase } from "./battle-phase";
import * as Utils from "#app/utils.js";
import * as Utils from "#app/utils";
import { CheckSwitchPhase } from "./check-switch-phase";
import { GameOverPhase } from "./game-over-phase";
import { PostSummonPhase } from "./post-summon-phase";
@ -358,24 +358,29 @@ export class EncounterPhase extends BattlePhase {
case BattleSpec.FINAL_BOSS:
const enemy = this.scene.getEnemyPokemon();
this.scene.ui.showText(this.getEncounterMessage(), null, () => {
const count = 5643853 + this.scene.gameData.gameStats.classicSessionsPlayed;
//The two lines below check if English ordinals (1st, 2nd, 3rd, Xth) are used and determine which one to use.
//Otherwise, it defaults to an empty string.
//As of 08-07-24: Spanish and Italian default to the English translations
const ordinalUse = ["en", "es", "it"];
const currentLanguage = i18next.resolvedLanguage ?? "en";
const ordinalIndex = (ordinalUse.includes(currentLanguage)) ? ["st", "nd", "rd"][((count + 90) % 100 - 10) % 10 - 1] ?? "th" : "";
const cycleCount = count.toLocaleString() + ordinalIndex;
const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET;
const genderStr = PlayerGender[genderIndex].toLowerCase();
const encounterDialogue = i18next.t("battleSpecDialogue:encounter", { context: genderStr, cycleCount: cycleCount });
this.scene.ui.showDialogue(encounterDialogue, enemy?.species.name, null, () => {
const localizationKey = "battleSpecDialogue:encounter";
if (this.scene.ui.shouldSkipDialogue(localizationKey)) {
// Logging mirrors logging found in dialogue-ui-handler
console.log(`Dialogue ${localizationKey} skipped`);
this.doEncounterCommon(false);
});
} else {
const count = 5643853 + this.scene.gameData.gameStats.classicSessionsPlayed;
// The line below checks if an English ordinal is necessary or not based on whether an entry for encounterLocalizationKey exists in the language or not.
const ordinalUsed = !i18next.exists(localizationKey, {fallbackLng: []}) || i18next.resolvedLanguage === "en" ? i18next.t("battleSpecDialogue:key", { count: count, ordinal: true }) : "";
const cycleCount = count.toLocaleString() + ordinalUsed;
const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET;
const genderStr = PlayerGender[genderIndex].toLowerCase();
const encounterDialogue = i18next.t(localizationKey, { context: genderStr, cycleCount: cycleCount });
if (!this.scene.gameData.getSeenDialogues()[localizationKey]) {
this.scene.gameData.saveSeenDialogue(localizationKey);
}
this.scene.ui.showDialogue(encounterDialogue, enemy?.species.name, null, () => {
this.doEncounterCommon(false);
});
}
}, 1500, true);
return true;
}
return false;
}
}

View File

@ -5,8 +5,9 @@ import { pokemonEvolutions } from "#app/data/pokemon-evolutions";
import i18next from "i18next";
import * as Utils from "../utils";
import { PlayerGender } from "#enums/player-gender";
import { Challenge, FreshStartChallenge, SingleGenerationChallenge, SingleTypeChallenge } from "#app/data/challenge.js";
import { ConditionFn } from "#app/@types/common.js";
import { Challenge, FreshStartChallenge, InverseBattleChallenge, SingleGenerationChallenge, SingleTypeChallenge } from "#app/data/challenge";
import { Challenges } from "#app/enums/challenges";
import { ConditionFn } from "#app/@types/common";
export enum AchvTier {
COMMON,
@ -137,8 +138,8 @@ export class ModifierAchv extends Achv {
}
export class ChallengeAchv extends Achv {
constructor(localizationKey: string, name: string, description: string, iconImage: string, score: integer, challengeFunc: (challenge: Challenge) => boolean) {
super(localizationKey, name, description, iconImage, score, (_scene: BattleScene, args: any[]) => challengeFunc((args[0] as Challenge)));
constructor(localizationKey: string, name: string, description: string, iconImage: string, score: integer, challengeFunc: (challenge: Challenge, scene: BattleScene) => boolean) {
super(localizationKey, name, description, iconImage, score, (_scene: BattleScene, args: any[]) => challengeFunc(args[0] as Challenge, _scene));
}
}
@ -275,6 +276,8 @@ export function getAchievementDescription(localizationKey: string): string {
return i18next.t("achv:MonoType.description", { context: genderStr, "type": i18next.t(`pokemonInfo:Type.${localizationKey.slice(5)}`) });
case "FRESH_START":
return i18next.t("achv:FRESH_START.description", { context: genderStr });
case "INVERSE_BATTLE":
return i18next.t("achv:INVERSE_BATTLE.description", { context: genderStr });
default:
return "";
}
@ -323,34 +326,35 @@ export const achvs = {
PERFECT_IVS: new Achv("PERFECT_IVS", "", "PERFECT_IVS.description", "blunder_policy", 100),
CLASSIC_VICTORY: new Achv("CLASSIC_VICTORY", "", "CLASSIC_VICTORY.description", "relic_crown", 150),
UNEVOLVED_CLASSIC_VICTORY: new Achv("UNEVOLVED_CLASSIC_VICTORY", "", "UNEVOLVED_CLASSIC_VICTORY.description", "eviolite", 175, c => c.getParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)),
MONO_GEN_ONE_VICTORY: new ChallengeAchv("MONO_GEN_ONE", "", "MONO_GEN_ONE.description", "ribbon_gen1", 100, c => c instanceof SingleGenerationChallenge && c.value === 1),
MONO_GEN_TWO_VICTORY: new ChallengeAchv("MONO_GEN_TWO", "", "MONO_GEN_TWO.description", "ribbon_gen2", 100, c => c instanceof SingleGenerationChallenge && c.value === 2),
MONO_GEN_THREE_VICTORY: new ChallengeAchv("MONO_GEN_THREE", "", "MONO_GEN_THREE.description", "ribbon_gen3", 100, c => c instanceof SingleGenerationChallenge && c.value === 3),
MONO_GEN_FOUR_VICTORY: new ChallengeAchv("MONO_GEN_FOUR", "", "MONO_GEN_FOUR.description", "ribbon_gen4", 100, c => c instanceof SingleGenerationChallenge && c.value === 4),
MONO_GEN_FIVE_VICTORY: new ChallengeAchv("MONO_GEN_FIVE", "", "MONO_GEN_FIVE.description", "ribbon_gen5", 100, c => c instanceof SingleGenerationChallenge && c.value === 5),
MONO_GEN_SIX_VICTORY: new ChallengeAchv("MONO_GEN_SIX", "", "MONO_GEN_SIX.description", "ribbon_gen6", 100, c => c instanceof SingleGenerationChallenge && c.value === 6),
MONO_GEN_SEVEN_VICTORY: new ChallengeAchv("MONO_GEN_SEVEN", "", "MONO_GEN_SEVEN.description", "ribbon_gen7", 100, c => c instanceof SingleGenerationChallenge && c.value === 7),
MONO_GEN_EIGHT_VICTORY: new ChallengeAchv("MONO_GEN_EIGHT", "", "MONO_GEN_EIGHT.description", "ribbon_gen8", 100, c => c instanceof SingleGenerationChallenge && c.value === 8),
MONO_GEN_NINE_VICTORY: new ChallengeAchv("MONO_GEN_NINE", "", "MONO_GEN_NINE.description", "ribbon_gen9", 100, c => c instanceof SingleGenerationChallenge && c.value === 9),
MONO_NORMAL: new ChallengeAchv("MONO_NORMAL", "", "MONO_NORMAL.description", "silk_scarf", 100, c => c instanceof SingleTypeChallenge && c.value === 1),
MONO_FIGHTING: new ChallengeAchv("MONO_FIGHTING", "", "MONO_FIGHTING.description", "black_belt", 100, c => c instanceof SingleTypeChallenge && c.value === 2),
MONO_FLYING: new ChallengeAchv("MONO_FLYING", "", "MONO_FLYING.description", "sharp_beak", 100, c => c instanceof SingleTypeChallenge && c.value === 3),
MONO_POISON: new ChallengeAchv("MONO_POISON", "", "MONO_POISON.description", "poison_barb", 100, c => c instanceof SingleTypeChallenge && c.value === 4),
MONO_GROUND: new ChallengeAchv("MONO_GROUND", "", "MONO_GROUND.description", "soft_sand", 100, c => c instanceof SingleTypeChallenge && c.value === 5),
MONO_ROCK: new ChallengeAchv("MONO_ROCK", "", "MONO_ROCK.description", "hard_stone", 100, c => c instanceof SingleTypeChallenge && c.value === 6),
MONO_BUG: new ChallengeAchv("MONO_BUG", "", "MONO_BUG.description", "silver_powder", 100, c => c instanceof SingleTypeChallenge && c.value === 7),
MONO_GHOST: new ChallengeAchv("MONO_GHOST", "", "MONO_GHOST.description", "spell_tag", 100, c => c instanceof SingleTypeChallenge && c.value === 8),
MONO_STEEL: new ChallengeAchv("MONO_STEEL", "", "MONO_STEEL.description", "metal_coat", 100, c => c instanceof SingleTypeChallenge && c.value === 9),
MONO_FIRE: new ChallengeAchv("MONO_FIRE", "", "MONO_FIRE.description", "charcoal", 100, c => c instanceof SingleTypeChallenge && c.value === 10),
MONO_WATER: new ChallengeAchv("MONO_WATER", "", "MONO_WATER.description", "mystic_water", 100, c => c instanceof SingleTypeChallenge && c.value === 11),
MONO_GRASS: new ChallengeAchv("MONO_GRASS", "", "MONO_GRASS.description", "miracle_seed", 100, c => c instanceof SingleTypeChallenge && c.value === 12),
MONO_ELECTRIC: new ChallengeAchv("MONO_ELECTRIC", "", "MONO_ELECTRIC.description", "magnet", 100, c => c instanceof SingleTypeChallenge && c.value === 13),
MONO_PSYCHIC: new ChallengeAchv("MONO_PSYCHIC", "", "MONO_PSYCHIC.description", "twisted_spoon", 100, c => c instanceof SingleTypeChallenge && c.value === 14),
MONO_ICE: new ChallengeAchv("MONO_ICE", "", "MONO_ICE.description", "never_melt_ice", 100, c => c instanceof SingleTypeChallenge && c.value === 15),
MONO_DRAGON: new ChallengeAchv("MONO_DRAGON", "", "MONO_DRAGON.description", "dragon_fang", 100, c => c instanceof SingleTypeChallenge && c.value === 16),
MONO_DARK: new ChallengeAchv("MONO_DARK", "", "MONO_DARK.description", "black_glasses", 100, c => c instanceof SingleTypeChallenge && c.value === 17),
MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, c => c instanceof SingleTypeChallenge && c.value === 18),
FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, c => c instanceof FreshStartChallenge && c.value === 1),
MONO_GEN_ONE_VICTORY: new ChallengeAchv("MONO_GEN_ONE", "", "MONO_GEN_ONE.description", "ribbon_gen1", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 1 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GEN_TWO_VICTORY: new ChallengeAchv("MONO_GEN_TWO", "", "MONO_GEN_TWO.description", "ribbon_gen2", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 2 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GEN_THREE_VICTORY: new ChallengeAchv("MONO_GEN_THREE", "", "MONO_GEN_THREE.description", "ribbon_gen3", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 3 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GEN_FOUR_VICTORY: new ChallengeAchv("MONO_GEN_FOUR", "", "MONO_GEN_FOUR.description", "ribbon_gen4", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 4 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GEN_FIVE_VICTORY: new ChallengeAchv("MONO_GEN_FIVE", "", "MONO_GEN_FIVE.description", "ribbon_gen5", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 5 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GEN_SIX_VICTORY: new ChallengeAchv("MONO_GEN_SIX", "", "MONO_GEN_SIX.description", "ribbon_gen6", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 6 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GEN_SEVEN_VICTORY: new ChallengeAchv("MONO_GEN_SEVEN", "", "MONO_GEN_SEVEN.description", "ribbon_gen7", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 7 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GEN_EIGHT_VICTORY: new ChallengeAchv("MONO_GEN_EIGHT", "", "MONO_GEN_EIGHT.description", "ribbon_gen8", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 8 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GEN_NINE_VICTORY: new ChallengeAchv("MONO_GEN_NINE", "", "MONO_GEN_NINE.description", "ribbon_gen9", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 9 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_NORMAL: new ChallengeAchv("MONO_NORMAL", "", "MONO_NORMAL.description", "silk_scarf", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 1 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_FIGHTING: new ChallengeAchv("MONO_FIGHTING", "", "MONO_FIGHTING.description", "black_belt", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 2 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_FLYING: new ChallengeAchv("MONO_FLYING", "", "MONO_FLYING.description", "sharp_beak", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 3 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_POISON: new ChallengeAchv("MONO_POISON", "", "MONO_POISON.description", "poison_barb", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 4 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GROUND: new ChallengeAchv("MONO_GROUND", "", "MONO_GROUND.description", "soft_sand", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 5 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_ROCK: new ChallengeAchv("MONO_ROCK", "", "MONO_ROCK.description", "hard_stone", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 6 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_BUG: new ChallengeAchv("MONO_BUG", "", "MONO_BUG.description", "silver_powder", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 7 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GHOST: new ChallengeAchv("MONO_GHOST", "", "MONO_GHOST.description", "spell_tag", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 8 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_STEEL: new ChallengeAchv("MONO_STEEL", "", "MONO_STEEL.description", "metal_coat", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 9 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_FIRE: new ChallengeAchv("MONO_FIRE", "", "MONO_FIRE.description", "charcoal", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 10 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_WATER: new ChallengeAchv("MONO_WATER", "", "MONO_WATER.description", "mystic_water", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 11 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_GRASS: new ChallengeAchv("MONO_GRASS", "", "MONO_GRASS.description", "miracle_seed", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 12 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_ELECTRIC: new ChallengeAchv("MONO_ELECTRIC", "", "MONO_ELECTRIC.description", "magnet", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 13 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_PSYCHIC: new ChallengeAchv("MONO_PSYCHIC", "", "MONO_PSYCHIC.description", "twisted_spoon", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 14 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_ICE: new ChallengeAchv("MONO_ICE", "", "MONO_ICE.description", "never_melt_ice", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 15 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_DRAGON: new ChallengeAchv("MONO_DRAGON", "", "MONO_DRAGON.description", "dragon_fang", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 16 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_DARK: new ChallengeAchv("MONO_DARK", "", "MONO_DARK.description", "black_glasses", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 17 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 18 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c, scene) => c instanceof FreshStartChallenge && c.value > 0 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0),
};
export function initAchievements() {

View File

@ -243,6 +243,8 @@ export class StarterPrefs {
if (pStr !== StarterPrefers_private_latest) {
// something changed, store the update
localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr);
// update the latest prefs
StarterPrefers_private_latest = pStr;
}
}
}

View File

@ -0,0 +1,203 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { Type } from "#app/data/type";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { Challenges } from "#enums/challenges";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
const TIMEOUT = 20 * 1000;
describe("Inverse Battle", () => {
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.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
game.override
.battleType("single")
.starterSpecies(Species.FEEBAS)
.ability(Abilities.BALL_FETCH)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH);
});
it("1. immune types are 2x effective - Thunderbolt against Ground Type", async () => {
game.override.enemySpecies(Species.SANDSHREW);
await game.challengeMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy.getMoveEffectiveness(player, allMoves[Moves.THUNDERBOLT])).toBe(2);
}, TIMEOUT);
it("2. 2x effective types are 0.5x effective - Thunderbolt against Flying Type", async () => {
game.override.enemySpecies(Species.PIDGEY);
await game.challengeMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy.getMoveEffectiveness(player, allMoves[Moves.THUNDERBOLT])).toBe(0.5);
}, TIMEOUT);
it("3. 0.5x effective types are 2x effective - Thunderbolt against Electric Type", async () => {
game.override.enemySpecies(Species.CHIKORITA);
await game.challengeMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy.getMoveEffectiveness(player, allMoves[Moves.THUNDERBOLT])).toBe(2);
}, TIMEOUT);
it("4. Stealth Rock follows the inverse matchups - Stealth Rock against Charizard deals 1/32 of max HP", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0);
game.override
.enemySpecies(Species.CHARIZARD)
.enemyLevel(100);
await game.challengeMode.startBattle();
const charizard = game.scene.getEnemyPokemon()!;
const maxHp = charizard.getMaxHp();
const damage_prediction = Math.max(Math.round(charizard.getMaxHp() / 32), 1);
console.log("Damage calcuation before round: " + charizard.getMaxHp() / 32);
const currentHp = charizard.hp;
const expectedHP = maxHp - damage_prediction;
console.log("Charizard's max HP: " + maxHp, "Damage: " + damage_prediction, "Current HP: " + currentHp, "Expected HP: " + expectedHP);
expect(currentHp).toBeGreaterThan(maxHp * 31 / 32 - 1);
}, TIMEOUT);
it("5. Freeze Dry is 2x effective against Water Type like other Ice type Move - Freeze Dry against Squirtle", async () => {
game.override.enemySpecies(Species.SQUIRTLE);
await game.challengeMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy.getMoveEffectiveness(player, allMoves[Moves.FREEZE_DRY])).toBe(2);
}, TIMEOUT);
it("6. Water Absorb should heal against water moves - Water Absorb against Water gun", async () => {
game.override
.moveset([Moves.WATER_GUN])
.enemyAbility(Abilities.WATER_ABSORB);
await game.challengeMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
enemy.hp = enemy.getMaxHp() - 1;
game.move.select(Moves.WATER_GUN);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEndPhase);
expect(enemy.hp).toBe(enemy.getMaxHp());
}, TIMEOUT);
it("7. Fire type does not get burned - Will-O-Wisp against Charmander", async () => {
game.override
.moveset([Moves.WILL_O_WISP])
.enemySpecies(Species.CHARMANDER);
await game.challengeMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.WILL_O_WISP);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.move.forceHit();
await game.phaseInterceptor.to(MoveEndPhase);
expect(enemy.status?.effect).not.toBe(StatusEffect.BURN);
}, TIMEOUT);
it("8. Electric type does not get paralyzed - Nuzzle against Pikachu", async () => {
game.override
.moveset([Moves.NUZZLE])
.enemySpecies(Species.PIKACHU)
.enemyLevel(50);
await game.challengeMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.NUZZLE);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEndPhase);
expect(enemy.status?.effect).not.toBe(StatusEffect.PARALYSIS);
}, TIMEOUT);
it("10. Anticipation should trigger on 2x effective moves - Anticipation against Thunderbolt", async () => {
game.override
.moveset([Moves.THUNDERBOLT])
.enemySpecies(Species.SANDSHREW)
.enemyAbility(Abilities.ANTICIPATION);
await game.challengeMode.startBattle();
expect(game.scene.getEnemyPokemon()?.summonData.abilitiesApplied[0]).toBe(Abilities.ANTICIPATION);
}, TIMEOUT);
it("11. Conversion 2 should change the type to the resistive type - Conversion 2 against Dragonite", async () => {
game.override
.moveset([Moves.CONVERSION_2])
.enemyMoveset([Moves.DRAGON_CLAW, Moves.DRAGON_CLAW, Moves.DRAGON_CLAW, Moves.DRAGON_CLAW]);
await game.challengeMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
game.move.select(Moves.CONVERSION_2);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getTypes()[0]).toBe(Type.DRAGON);
}, TIMEOUT);
it("12. Flying Press should be 0.25x effective against Grass + Dark Type - Flying Press against Meowscarada", async () => {
game.override
.moveset([Moves.FLYING_PRESS])
.enemySpecies(Species.MEOWSCARADA);
await game.challengeMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy.getMoveEffectiveness(player, allMoves[Moves.FLYING_PRESS])).toBe(0.25);
}, TIMEOUT);
});

View File

@ -0,0 +1,87 @@
import { allMoves } from "#app/data/move.js";
import { StatusEffect } from "#app/enums/status-effect.js";
import { CommandPhase } from "#app/phases/command-phase.js";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Sparkly Swirl", () => {
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
.enemySpecies(Species.SHUCKLE)
.enemyLevel(100)
.enemyMoveset(SPLASH_ONLY)
.enemyAbility(Abilities.BALL_FETCH)
.moveset([Moves.SPARKLY_SWIRL, Moves.SPLASH])
.ability(Abilities.BALL_FETCH);
vi.spyOn(allMoves[Moves.SPARKLY_SWIRL], "accuracy", "get").mockReturnValue(100);
});
it("should cure status effect of the user, its ally, and all party pokemon", async () => {
game.override
.battleType("double")
.statusEffect(StatusEffect.BURN);
await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA, Species.RATTATA]);
const [leftPlayer, rightPlayer, partyPokemon] = game.scene.getParty();
const leftOpp = game.scene.getEnemyPokemon()!;
vi.spyOn(leftPlayer, "resetStatus");
vi.spyOn(rightPlayer, "resetStatus");
vi.spyOn(partyPokemon, "resetStatus");
game.move.select(Moves.SPARKLY_SWIRL, 0, leftOpp.getBattlerIndex());
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH, 1);
await game.toNextTurn();
expect(leftPlayer.resetStatus).toHaveBeenCalledOnce();
expect(rightPlayer.resetStatus).toHaveBeenCalledOnce();
expect(partyPokemon.resetStatus).toHaveBeenCalledOnce();
expect(leftPlayer.status?.effect).toBeUndefined();
expect(rightPlayer.status?.effect).toBeUndefined();
expect(partyPokemon.status?.effect).toBeUndefined();
});
it("should not cure status effect of the target/target's allies", async () => {
game.override
.battleType("double")
.enemyStatusEffect(StatusEffect.BURN);
await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA]);
const [leftOpp, rightOpp] = game.scene.getEnemyField();
vi.spyOn(leftOpp, "resetStatus");
vi.spyOn(rightOpp, "resetStatus");
game.move.select(Moves.SPARKLY_SWIRL, 0, leftOpp.getBattlerIndex());
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH, 1);
await game.toNextTurn();
expect(leftOpp.resetStatus).toHaveBeenCalledTimes(0);
expect(rightOpp.resetStatus).toHaveBeenCalledTimes(0);
expect(leftOpp.status?.effect).toBeTruthy();
expect(rightOpp.status?.effect).toBeTruthy();
expect(leftOpp.status?.effect).toBe(StatusEffect.BURN);
expect(rightOpp.status?.effect).toBe(StatusEffect.BURN);
});
});

View File

@ -40,6 +40,7 @@ import fs from "fs";
import { vi } from "vitest";
import { ClassicModeHelper } from "./helpers/classicModeHelper";
import { DailyModeHelper } from "./helpers/dailyModeHelper";
import { ChallengeModeHelper } from "./helpers/challengeModeHelper";
import { MoveHelper } from "./helpers/moveHelper";
import { OverridesHelper } from "./helpers/overridesHelper";
import { SettingsHelper } from "./helpers/settingsHelper";
@ -57,6 +58,7 @@ export default class GameManager {
public readonly move: MoveHelper;
public readonly classicMode: ClassicModeHelper;
public readonly dailyMode: DailyModeHelper;
public readonly challengeMode: ChallengeModeHelper;
public readonly settings: SettingsHelper;
/**
@ -77,6 +79,7 @@ export default class GameManager {
this.move = new MoveHelper(this);
this.classicMode = new ClassicModeHelper(this);
this.dailyMode = new DailyModeHelper(this);
this.challengeMode = new ChallengeModeHelper(this);
this.settings = new SettingsHelper(this);
}

View File

@ -0,0 +1,78 @@
import { BattleStyle } from "#app/enums/battle-style";
import { Species } from "#app/enums/species";
import overrides from "#app/overrides";
import { EncounterPhase } from "#app/phases/encounter-phase";
import { SelectStarterPhase } from "#app/phases/select-starter-phase";
import { Mode } from "#app/ui/ui";
import { generateStarter } from "../gameManagerUtils";
import { GameManagerHelper } from "./gameManagerHelper";
import { Challenge } from "#app/data/challenge";
import { CommandPhase } from "#app/phases/command-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { Challenges } from "#enums/challenges";
import { copyChallenge } from "data/challenge";
/**
* Helper to handle Challenge mode specifics
*/
export class ChallengeModeHelper extends GameManagerHelper {
challenges: Challenge[] = [];
/**
* Adds a challenge to the challenge mode helper.
* @param id - The challenge id.
* @param value - The challenge value.
* @param severity - The challenge severity.
*/
addChallenge(id: Challenges, value: number, severity: number) {
const challenge = copyChallenge({ id, value, severity });
this.challenges.push(challenge);
}
/**
* Runs the Challenge game to the summon phase.
* @param gameMode - Optional game mode to set.
* @returns A promise that resolves when the summon phase is reached.
*/
async runToSummon(species?: Species[]) {
await this.game.runToTitle();
this.game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
this.game.scene.gameMode.challenges = this.challenges;
const starters = generateStarter(this.game.scene, species);
const selectStarterPhase = new SelectStarterPhase(this.game.scene);
this.game.scene.pushPhase(new EncounterPhase(this.game.scene, false));
selectStarterPhase.initBattle(starters);
});
await this.game.phaseInterceptor.run(EncounterPhase);
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) {
this.game.removeEnemyHeldItems();
}
}
/**
* Transitions to the start of a battle.
* @param species - Optional array of species to start the battle with.
* @returns A promise that resolves when the battle is started.
*/
async startBattle(species?: Species[]) {
await this.runToSummon(species);
if (this.game.scene.battleStyle === BattleStyle.SWITCH) {
this.game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => {
this.game.setMode(Mode.MESSAGE);
this.game.endPhase();
}, () => this.game.isCurrentPhase(CommandPhase) || this.game.isCurrentPhase(TurnInitPhase));
this.game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => {
this.game.setMode(Mode.MESSAGE);
this.game.endPhase();
}, () => this.game.isCurrentPhase(CommandPhase) || this.game.isCurrentPhase(TurnInitPhase));
}
await this.game.phaseInterceptor.to(CommandPhase);
console.log("==================[New Turn]==================");
}
}

View File

@ -77,7 +77,21 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
}
protected setupOptions() {
const options = this.config?.options || [];
const configOptions = this.config?.options ?? [];
let options: OptionSelectItem[];
// for performance reasons, this limits how many options we can see at once. Without this, it would try to make text options for every single options
// which makes the performance take a hit. If there's not enough options to do this (set to 10 at the moment) and the ui mode !== Mode.AUTO_COMPLETE,
// this is ignored and the original code is untouched, with the options array being all the options from the config
if (configOptions.length >= 10 && this.scene.ui.getMode() === Mode.AUTO_COMPLETE) {
const optionsScrollTotal = configOptions.length;
const optionStartIndex = this.scrollCursor;
const optionEndIndex = Math.min(optionsScrollTotal, optionStartIndex + (!optionStartIndex || this.scrollCursor + (this.config?.maxOptions! - 1) >= optionsScrollTotal ? this.config?.maxOptions! - 1 : this.config?.maxOptions! - 2));
options = configOptions.slice(optionStartIndex, optionEndIndex + 2);
} else {
options = configOptions;
}
if (this.optionSelectText) {
this.optionSelectText.destroy();
@ -192,6 +206,19 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
} else {
ui.playError();
}
} else if (button === Button.SUBMIT && ui.getMode() === Mode.AUTO_COMPLETE) {
// this is here to differentiate between a Button.SUBMIT vs Button.ACTION within the autocomplete handler
// this is here because Button.ACTION is picked up as z on the keyboard, meaning if you're typing and hit z, it'll select the option you've chosen
success = true;
const option = this.config?.options[this.cursor + (this.scrollCursor - (this.scrollCursor ? 1 : 0))];
if (option?.handler()) {
if (!option.keepOpen) {
this.clear();
}
playSound = !option.overrideSound;
} else {
ui.playError();
}
} else {
switch (button) {
case Button.UP:

View File

@ -64,12 +64,15 @@ export default class AdminUiHandler extends FormModalUiHandler {
Utils.apiPost("admin/account/discord-link", `username=${encodeURIComponent(this.inputs[0].text)}&discordId=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded", true)
.then(response => {
if (!response.ok) {
return response.text();
console.error(response);
}
return response.json();
this.inputs[0].setText("");
this.inputs[1].setText("");
this.scene.ui.revertMode();
})
.then(response => {
this.scene.ui.setMode(Mode.ADMIN, config);
.catch((err) => {
console.error(err);
this.scene.ui.revertMode();
});
return false;
};

View File

@ -0,0 +1,45 @@
import { Button } from "#enums/buttons";
import BattleScene from "../battle-scene";
import AbstractOptionSelectUiHandler from "./abstact-option-select-ui-handler";
import { Mode } from "./ui";
export default class AutoCompleteUiHandler extends AbstractOptionSelectUiHandler {
modalContainer: Phaser.GameObjects.Container;
constructor(scene: BattleScene, mode: Mode = Mode.OPTION_SELECT) {
super(scene, mode);
}
getWindowWidth(): integer {
return 64;
}
show(args: any[]): boolean {
if (args[0].modalContainer) {
const { modalContainer } = args[0];
const show = super.show(args);
this.modalContainer = modalContainer;
this.setupOptions();
return show;
}
return false;
}
protected setupOptions() {
super.setupOptions();
if (this.modalContainer) {
this.optionSelectContainer.setSize(this.optionSelectContainer.height, Math.max(this.optionSelectText.displayWidth + 24, this.getWindowWidth()));
this.optionSelectContainer.setPositionRelative(this.modalContainer, this.optionSelectBg.width, this.optionSelectBg.height + 50);
}
}
processInput(button: Button): boolean {
// the cancel and action button are here because if you're typing, x and z are used for cancel/action. This means you could be typing something and accidentally cancel/select when you don't mean to
// the submit button is therefore used to select a choice (the enter button), though this does not work on my local dev testing for phones, as for my phone/keyboard combo, the enter and z key are both
// bound to Button.ACTION, which makes this not work on mobile
if (button !== Button.CANCEL && button !== Button.ACTION) {
return super.processInput(button);
}
return false;
}
}

View File

@ -21,6 +21,8 @@ export default class BattleMessageUiHandler extends MessageUiHandler {
public movesWindowContainer: Phaser.GameObjects.Container;
public nameBoxContainer: Phaser.GameObjects.Container;
public readonly wordWrapWidth: number = 1780;
constructor(scene: BattleScene) {
super(scene, Mode.MESSAGE);
}
@ -63,7 +65,7 @@ export default class BattleMessageUiHandler extends MessageUiHandler {
const message = addTextObject(this.scene, 0, 0, "", TextStyle.MESSAGE, {
maxLines: 2,
wordWrap: {
width: 1780
width: this.wordWrapWidth
}
});
messageContainer.add(message);
@ -129,7 +131,7 @@ export default class BattleMessageUiHandler extends MessageUiHandler {
this.commandWindow.setVisible(false);
this.movesWindowContainer.setVisible(false);
this.message.setWordWrapWidth(1780);
this.message.setWordWrapWidth(this.wordWrapWidth);
return true;
}
@ -161,7 +163,9 @@ export default class BattleMessageUiHandler extends MessageUiHandler {
}
showDialogue(text: string, name?: string, delay?: integer | null, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) {
name && this.showNameText(name);
if (name) {
this.showNameText(name);
}
super.showDialogue(text, name, delay, callback, callbackDelay, prompt, promptDelay);
}

View File

@ -2,7 +2,7 @@ import BattleScene, { bypassLogin } from "../battle-scene";
import { TextStyle, addTextObject, getTextStyleOptions } from "./text";
import { Mode } from "./ui";
import * as Utils from "../utils";
import { addWindow } from "./ui-theme";
import { addWindow, WindowVariant } from "./ui-theme";
import MessageUiHandler from "./message-ui-handler";
import { OptionSelectConfig, OptionSelectItem } from "./abstact-option-select-ui-handler";
import { Tutorial, handleTutorial } from "../tutorial";
@ -11,6 +11,7 @@ import i18next from "i18next";
import { Button } from "#enums/buttons";
import { GameDataType } from "#enums/game-data-type";
import BgmBar from "#app/ui/bgm-bar";
import AwaitableUiHandler from "./awaitable-ui-handler";
enum MenuOptions {
GAME_SETTINGS,
@ -31,6 +32,10 @@ const githubUrl = "https://github.com/pagefaultgames/pokerogue";
const redditUrl = "https://www.reddit.com/r/pokerogue";
export default class MenuUiHandler extends MessageUiHandler {
private readonly textPadding = 8;
private readonly defaultMessageBoxWidth = 220;
private readonly defaultWordWrapWidth = 1224;
private menuContainer: Phaser.GameObjects.Container;
private menuMessageBoxContainer: Phaser.GameObjects.Container;
private menuOverlay: Phaser.GameObjects.Rectangle;
@ -46,17 +51,20 @@ export default class MenuUiHandler extends MessageUiHandler {
protected manageDataConfig: OptionSelectConfig;
protected communityConfig: OptionSelectConfig;
// Windows for the default message box and the message box for testing dialogue
private menuMessageBox: Phaser.GameObjects.NineSlice;
private dialogueMessageBox: Phaser.GameObjects.NineSlice;
protected scale: number = 0.1666666667;
public bgmBar: BgmBar;
constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode);
this.excludedMenus = () => [
{ condition: [Mode.COMMAND, Mode.TITLE].includes(mode ?? Mode.TITLE), options: [ MenuOptions.EGG_GACHA, MenuOptions.EGG_LIST] },
{ condition: bypassLogin, options: [ MenuOptions.LOG_OUT ] }
{ condition: [Mode.COMMAND, Mode.TITLE].includes(mode ?? Mode.TITLE), options: [MenuOptions.EGG_GACHA, MenuOptions.EGG_LIST] },
{ condition: bypassLogin, options: [MenuOptions.LOG_OUT] }
];
this.menuOptions = Utils.getEnumKeys(MenuOptions)
@ -98,8 +106,8 @@ export default class MenuUiHandler extends MessageUiHandler {
render() {
const ui = this.getUi();
this.excludedMenus = () => [
{ condition: ![Mode.COMMAND, Mode.TITLE].includes(ui.getModeChain()[0]), options: [ MenuOptions.EGG_GACHA, MenuOptions.EGG_LIST] },
{ condition: bypassLogin, options: [ MenuOptions.LOG_OUT ] }
{ condition: ![Mode.COMMAND, Mode.TITLE].includes(ui.getModeChain()[0]), options: [MenuOptions.EGG_GACHA, MenuOptions.EGG_LIST] },
{ condition: bypassLogin, options: [MenuOptions.LOG_OUT] }
];
this.menuOptions = Utils.getEnumKeys(MenuOptions)
@ -115,12 +123,12 @@ export default class MenuUiHandler extends MessageUiHandler {
this.menuBg = addWindow(this.scene,
(this.scene.game.canvas.width / 6) - (this.optionSelectText.displayWidth + 25),
0,
this.optionSelectText.displayWidth + 19+24*this.scale,
this.optionSelectText.displayWidth + 19 + 24 * this.scale,
(this.scene.game.canvas.height / 6) - 2
);
this.menuBg.setOrigin(0, 0);
this.optionSelectText.setPositionRelative(this.menuBg, 10+24*this.scale, 6);
this.optionSelectText.setPositionRelative(this.menuBg, 10 + 24 * this.scale, 6);
this.menuContainer.add(this.menuBg);
@ -131,20 +139,27 @@ export default class MenuUiHandler extends MessageUiHandler {
this.menuMessageBoxContainer = this.scene.add.container(0, 130);
this.menuMessageBoxContainer.setName("menu-message-box");
this.menuMessageBoxContainer.setVisible(false);
this.menuContainer.add(this.menuMessageBoxContainer);
const menuMessageBox = addWindow(this.scene, 0, -0, 220, 48);
menuMessageBox.setOrigin(0, 0);
this.menuMessageBoxContainer.add(menuMessageBox);
// Window for general messages
this.menuMessageBox = addWindow(this.scene, 0, 0, this.defaultMessageBoxWidth, 48);
this.menuMessageBox.setOrigin(0, 0);
this.menuMessageBoxContainer.add(this.menuMessageBox);
const menuMessageText = addTextObject(this.scene, 8, 8, "", TextStyle.WINDOW, { maxLines: 2 });
// Full-width window used for testing dialog messages in debug mode
this.dialogueMessageBox = addWindow(this.scene, -this.textPadding, 0, this.scene.game.canvas.width / 6 + this.textPadding * 2, 49, false, false, 0, 0, WindowVariant.THIN);
this.dialogueMessageBox.setOrigin(0, 0);
this.menuMessageBoxContainer.add(this.dialogueMessageBox);
const menuMessageText = addTextObject(this.scene, this.textPadding, this.textPadding, "", TextStyle.WINDOW, { maxLines: 2 });
menuMessageText.setName("menu-message");
menuMessageText.setWordWrapWidth(1224);
menuMessageText.setOrigin(0, 0);
this.menuMessageBoxContainer.add(menuMessageText);
this.message = menuMessageText;
// By default we use the general purpose message window
this.setDialogTestMode(false);
this.menuContainer.add(this.menuMessageBoxContainer);
const manageDataOptions: any[] = []; // TODO: proper type
@ -155,7 +170,7 @@ export default class MenuUiHandler extends MessageUiHandler {
const config: OptionSelectConfig = {
options: new Array(5).fill(null).map((_, i) => i).filter(slotFilter).map(i => {
return {
label: i18next.t("menuUiHandler:slot", {slotNumber: i+1}),
label: i18next.t("menuUiHandler:slot", { slotNumber: i + 1 }),
handler: () => {
callback(i);
ui.revertMode();
@ -257,8 +272,55 @@ export default class MenuUiHandler extends MessageUiHandler {
return true;
},
keepOpen: true
},
{
});
if (Utils.isLocal || Utils.isBeta) { // this should make sure we don't have this option in live
manageDataOptions.push({
label: "Test Dialogue",
handler: () => {
ui.playSelect();
const prefilledText = "";
const buttonAction: any = {};
buttonAction["buttonActions"] = [
(sanitizedName: string) => {
ui.revertMode();
ui.playSelect();
const dialogueTestName = sanitizedName;
const dialogueName = decodeURIComponent(escape(atob(dialogueTestName)));
const handler = ui.getHandler() as AwaitableUiHandler;
handler.tutorialActive = true;
const interpolatorOptions: any = {};
const splitArr = dialogueName.split(" "); // this splits our inputted text into words to cycle through later
const translatedString = splitArr[0]; // this is our outputted i18 string
const regex = RegExp("\\{\\{(\\w*)\\}\\}", "g"); // this is a regex expression to find all the text between {{ }} in the i18 output
const matches = i18next.t(translatedString).match(regex) ?? [];
if (matches.length > 0) {
for (let match = 0; match < matches.length; match++) {
// we add 1 here because splitArr[0] is our first value for the translatedString, and after that is where the variables are
// the regex here in the replace (/\W/g) is to remove the {{ and }} and just give us all alphanumeric characters
if (typeof splitArr[match + 1] !== "undefined") {
interpolatorOptions[matches[match].replace(/\W/g, "")] = i18next.t(splitArr[match + 1]);
}
}
}
// Switch to the dialog test window
this.setDialogTestMode(true);
ui.showText(String(i18next.t(translatedString, interpolatorOptions)), null, () => this.scene.ui.showText("", 0, () => {
handler.tutorialActive = false;
// Go back to the default message window
this.setDialogTestMode(false);
}), null, true);
},
() => {
ui.revertMode();
}
];
ui.setMode(Mode.TEST_DIALOGUE, buttonAction, prefilledText);
return true;
},
keepOpen: true
});
}
manageDataOptions.push({
label: i18next.t("menuUiHandler:cancel"),
handler: () => {
this.scene.ui.revertMode();
@ -421,7 +483,7 @@ export default class MenuUiHandler extends MessageUiHandler {
break;
case MenuOptions.MANAGE_DATA:
if (!bypassLogin && !this.manageDataConfig.options.some(o => o.label === i18next.t("menuUiHandler:linkDiscord") || o.label === i18next.t("menuUiHandler:unlinkDiscord"))) {
this.manageDataConfig.options.splice(this.manageDataConfig.options.length-1, 0,
this.manageDataConfig.options.splice(this.manageDataConfig.options.length - 1, 0,
{
label: loggedInUser?.discordId === "" ? i18next.t("menuUiHandler:linkDiscord") : i18next.t("menuUiHandler:unlinkDiscord"),
handler: () => {
@ -547,6 +609,21 @@ export default class MenuUiHandler extends MessageUiHandler {
return success || error;
}
/**
* Switch the message window style and size when we are replaying dialog for debug purposes
* In "dialog test mode", the window takes the whole width of the screen and the text
* is set up to wrap around the same way as the dialogue during the game
* @param isDialogMode whether to use the dialog test
*/
setDialogTestMode(isDialogMode: boolean) {
this.menuMessageBox.setVisible(!isDialogMode);
this.dialogueMessageBox.setVisible(isDialogMode);
// If we're testing dialog, we use the same word wrapping as the battle message handler
this.message.setWordWrapWidth(isDialogMode ? this.scene.ui.getMessageHandler().wordWrapWidth : this.defaultWordWrapWidth);
this.message.setX(isDialogMode ? this.textPadding + 1 : this.textPadding);
this.message.setY(isDialogMode ? this.textPadding + 0.4 : this.textPadding);
}
showText(text: string, delay?: number, callback?: Function, callbackDelay?: number, prompt?: boolean, promptDelay?: number): void {
this.menuMessageBoxContainer.setVisible(!!text);

View File

@ -374,23 +374,14 @@ export default class RunInfoUiHandler extends UiHandler {
case GameModes.CHALLENGE:
modeText.appendText(`${i18next.t("gameMode:challenge")}`, false);
modeText.appendText(`\t\t${i18next.t("runHistory:challengeRules")}: `);
const runChallenges = this.runInfo.challenges;
const rules: string[] = [];
for (let i = 0; i < runChallenges.length; i++) {
if (runChallenges[i].id === Challenges.SINGLE_GENERATION && runChallenges[i].value !== 0) {
rules.push(i18next.t(`runHistory:challengeMonoGen${runChallenges[i].value}`));
} else if (runChallenges[i].id === Challenges.SINGLE_TYPE && runChallenges[i].value !== 0) {
rules.push(i18next.t(`pokemonInfo:Type.${Type[runChallenges[i].value-1]}` as const));
} else if (runChallenges[i].id === Challenges.FRESH_START && runChallenges[i].value !== 0) {
rules.push(i18next.t("challenges:freshStart.name"));
}
}
const rules: string[] = this.challengeParser();
if (rules) {
for (let i = 0; i < rules.length; i++) {
const newline = i > 0 && i%2 === 0;
if (i > 0) {
modeText.appendText(" + ", false);
modeText.appendText(" + ", newline);
}
modeText.appendText(rules[i], false);
modeText.appendText(rules[i], newline);
}
}
break;
@ -466,6 +457,34 @@ export default class RunInfoUiHandler extends UiHandler {
this.runContainer.add(this.runInfoContainer);
}
/**
* This function parses the Challenges section of the Run Entry and returns a list of active challenge.
* @return string[] of active challenge names
*/
private challengeParser(): string[] {
const rules: string[] = [];
for (let i = 0; i < this.runInfo.challenges.length; i++) {
if (this.runInfo.challenges[i].value !== 0) {
switch (this.runInfo.challenges[i].id) {
case Challenges.SINGLE_GENERATION:
rules.push(i18next.t(`runHistory:challengeMonoGen${this.runInfo.challenges[i].value}`));
break;
case Challenges.SINGLE_TYPE:
rules.push(i18next.t(`pokemonInfo:Type.${Type[this.runInfo.challenges[i].value-1]}` as const));
break;
case Challenges.FRESH_START:
rules.push(i18next.t("challenges:freshStart.name"));
break;
case Challenges.INVERSE_BATTLE:
//
rules.push(i18next.t("challenges:inverseBattle.shortName").split("").reverse().join(""));
break;
}
}
}
return rules;
}
/**
* Parses and displays the run's player party.
* Default Information: Icon, Level, Nature, Ability, Passive, Shiny Status, Fusion Status, Stats, and Moves.

View File

@ -915,7 +915,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.allSpecies.forEach((species, s) => {
const icon = this.starterContainers[s].icon;
const dexEntry = this.scene.gameData.dexData[species.speciesId];
this.starterPreferences[species.speciesId] = this.starterPreferences[species.speciesId] ?? {};
// Initialize the StarterAttributes for this species
this.starterPreferences[species.speciesId] = this.initStarterPrefs(species);
if (dexEntry.caughtAttr) {
icon.clearTint();
@ -942,6 +944,93 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
return false;
}
/**
* Get the starter attributes for the given PokemonSpecies, after sanitizing them.
* If somehow a preference is set for a form, variant, gender, ability or nature
* that wasn't actually unlocked or is invalid it will be cleared here
*
* @param species The species to get Starter Preferences for
* @returns StarterAttributes for the species
*/
initStarterPrefs(species: PokemonSpecies): StarterAttributes {
const starterAttributes = this.starterPreferences[species.speciesId];
const dexEntry = this.scene.gameData.dexData[species.speciesId];
const starterData = this.scene.gameData.starterData[species.speciesId];
// no preferences or Pokemon wasn't caught, return empty attribute
if (!starterAttributes || !dexEntry.caughtAttr) {
return {};
}
const caughtAttr = dexEntry.caughtAttr;
const hasShiny = caughtAttr & DexAttr.SHINY;
const hasNonShiny = caughtAttr & DexAttr.NON_SHINY;
if (starterAttributes.shiny && !hasShiny) {
// shiny form wasn't unlocked, purging shiny and variant setting
delete starterAttributes.shiny;
delete starterAttributes.variant;
} else if (starterAttributes.shiny === false && !hasNonShiny) {
// non shiny form wasn't unlocked, purging shiny setting
delete starterAttributes.shiny;
}
if (starterAttributes.variant !== undefined && !isNaN(starterAttributes.variant)) {
const unlockedVariants = [
hasNonShiny,
hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT,
hasShiny && caughtAttr & DexAttr.VARIANT_2,
hasShiny && caughtAttr & DexAttr.VARIANT_3
];
if (!unlockedVariants[starterAttributes.variant + 1]) { // add 1 as -1 = non-shiny
// requested variant wasn't unlocked, purging setting
delete starterAttributes.variant;
}
}
if (starterAttributes.female !== undefined) {
if (!(starterAttributes.female ? caughtAttr & DexAttr.FEMALE : caughtAttr & DexAttr.MALE)) {
// requested gender wasn't unlocked, purging setting
delete starterAttributes.female;
}
}
if (starterAttributes.ability !== undefined) {
const speciesHasSingleAbility = species.ability2 === species.ability1;
const abilityAttr = starterData.abilityAttr;
const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1;
const hasAbility2 = abilityAttr & AbilityAttr.ABILITY_2;
const hasHiddenAbility = abilityAttr & AbilityAttr.ABILITY_HIDDEN;
// Due to a past bug it is possible that some Pokemon with a single ability have the ability2 flag
// In this case, we only count ability2 as valid if ability1 was not unlocked, otherwise we ignore it
const unlockedAbilities = [
hasAbility1,
speciesHasSingleAbility ? hasAbility2 && !hasAbility1 : hasAbility2,
hasHiddenAbility
];
if (!unlockedAbilities[starterAttributes.ability]) {
// requested ability wasn't unlocked, purging setting
delete starterAttributes.ability;
}
}
const selectedForm = starterAttributes.form;
if (selectedForm !== undefined && (!species.forms[selectedForm]?.isStarterSelectable || !(caughtAttr & this.scene.gameData.getFormAttr(selectedForm)))) {
// requested form wasn't unlocked/isn't a starter form, purging setting
delete starterAttributes.form;
}
if (starterAttributes.nature !== undefined) {
const unlockedNatures = this.scene.gameData.getNaturesForAttr(dexEntry.natureAttr);
if (unlockedNatures.indexOf(starterAttributes.nature as unknown as Nature) < 0) {
// requested nature wasn't unlocked, purging setting
delete starterAttributes.nature;
}
}
return starterAttributes;
}
/**
* Set the selections for all filters to their default starting value
*/
@ -1749,9 +1838,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
switch (button) {
case Button.CYCLE_SHINY:
if (this.canCycleShiny) {
const newVariant = props.variant;
const newVariant = starterAttributes.variant ? starterAttributes.variant as Variant : props.variant;
starterAttributes.shiny = starterAttributes.shiny ? !starterAttributes.shiny : true;
this.setSpeciesDetails(this.lastSpecies, !props.shiny, undefined, undefined, props.shiny ? 0 : undefined, undefined, undefined);
this.setSpeciesDetails(this.lastSpecies, !props.shiny, undefined, undefined, props.shiny ? 0 : newVariant, undefined, undefined);
if (starterAttributes.shiny) {
this.scene.playSound("se/sparkle");
// Set the variant label to the shiny tint
@ -1760,10 +1849,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonShinyIcon.setTint(tint);
this.pokemonShinyIcon.setVisible(true);
} else {
// starterAttributes.variant = 0;
if (starterAttributes?.variant) {
delete starterAttributes.variant;
}
this.pokemonShinyIcon.setVisible(false);
success = true;
}
@ -2276,43 +2361,39 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
container.cost = this.scene.gameData.getSpeciesStarterValue(container.species.speciesId);
// First, ensure you have the caught attributes for the species else default to bigint 0
const isCaught = this.scene.gameData.dexData[container.species.speciesId]?.caughtAttr || BigInt(0);
// Define the variables based on whether their respective variants have been caught
const isVariant3Caught = !!(isCaught & DexAttr.VARIANT_3);
const isVariant2Caught = !!(isCaught & DexAttr.VARIANT_2);
const isVariantCaught = !!(isCaught & DexAttr.SHINY);
const isUncaught = !isCaught && !isVariantCaught && !isVariant2Caught && !isVariant3Caught;
const isPassiveUnlocked = this.scene.gameData.starterData[container.species.speciesId].passiveAttr > 0;
const isPassiveUnlockable = this.isPassiveAvailable(container.species.speciesId) && !isPassiveUnlocked;
const isCostReduced = this.scene.gameData.starterData[container.species.speciesId].valueReduction > 0;
const isCostReductionUnlockable = this.isValueReductionAvailable(container.species.speciesId);
const isFavorite = this.starterPreferences[container.species.speciesId]?.favorite ?? false;
const isWin = this.scene.gameData.starterData[container.species.speciesId].classicWinCount > 0;
const isNotWin = this.scene.gameData.starterData[container.species.speciesId].classicWinCount === 0;
const isUndefined = this.scene.gameData.starterData[container.species.speciesId].classicWinCount === undefined;
const isHA = this.scene.gameData.starterData[container.species.speciesId].abilityAttr & AbilityAttr.ABILITY_HIDDEN;
const isEggPurchasable = this.isSameSpeciesEggAvailable(container.species.speciesId);
const caughtAttr = this.scene.gameData.dexData[container.species.speciesId]?.caughtAttr || BigInt(0);
const starterData = this.scene.gameData.starterData[container.species.speciesId];
// Gen filter
const fitsGen = this.filterBar.getVals(DropDownColumn.GEN).includes(container.species.generation);
// Type filter
const fitsType = this.filterBar.getVals(DropDownColumn.TYPES).some(type => container.species.isOfType((type as number) - 1));
// Caught / Shiny filter
const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY);
const isShinyCaught = !!(caughtAttr & DexAttr.SHINY);
const isVariant1Caught = isShinyCaught && !!(caughtAttr & DexAttr.DEFAULT_VARIANT);
const isVariant2Caught = isShinyCaught && !!(caughtAttr & DexAttr.VARIANT_2);
const isVariant3Caught = isShinyCaught && !!(caughtAttr & DexAttr.VARIANT_3);
const isUncaught = !isNonShinyCaught && !isVariant1Caught && !isVariant2Caught && !isVariant3Caught;
const fitsCaught = this.filterBar.getVals(DropDownColumn.CAUGHT).some(caught => {
if (caught === "SHINY3") {
return isVariant3Caught;
} else if (caught === "SHINY2") {
return isVariant2Caught && !isVariant3Caught;
} else if (caught === "SHINY") {
return isVariantCaught && !isVariant2Caught && !isVariant3Caught;
return isVariant1Caught && !isVariant2Caught && !isVariant3Caught;
} else if (caught === "NORMAL") {
return isCaught && !isVariantCaught && !isVariant2Caught && !isVariant3Caught;
return isNonShinyCaught && !isVariant1Caught && !isVariant2Caught && !isVariant3Caught;
} else if (caught === "UNCAUGHT") {
return isUncaught;
}
});
// Passive Filter
const isPassiveUnlocked = starterData.passiveAttr > 0;
const isPassiveUnlockable = this.isPassiveAvailable(container.species.speciesId) && !isPassiveUnlocked;
const fitsPassive = this.filterBar.getVals(DropDownColumn.UNLOCKS).some(unlocks => {
if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.ON) {
return isPassiveUnlocked;
@ -2325,6 +2406,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
});
// Cost Reduction Filter
const isCostReduced = starterData.valueReduction > 0;
const isCostReductionUnlockable = this.isValueReductionAvailable(container.species.speciesId);
const fitsCostReduction = this.filterBar.getVals(DropDownColumn.UNLOCKS).some(unlocks => {
if (unlocks.val === "COST_REDUCTION" && unlocks.state === DropDownState.ON) {
return isCostReduced;
@ -2337,6 +2421,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
});
// Favorite Filter
const isFavorite = this.starterPreferences[container.species.speciesId]?.favorite ?? false;
const fitsFavorite = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
if (misc.val === "FAVORITE" && misc.state === DropDownState.ON) {
return isFavorite;
@ -2349,28 +2435,34 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
});
// Ribbon / Classic Win Filter
const hasWon = starterData.classicWinCount > 0;
const hasNotWon = starterData.classicWinCount === 0;
const isUndefined = starterData.classicWinCount === undefined;
const fitsWin = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
if (container.species.speciesId < 10) {
}
if (misc.val === "WIN" && misc.state === DropDownState.ON) {
return isWin;
return hasWon;
} else if (misc.val === "WIN" && misc.state === DropDownState.EXCLUDE) {
return isNotWin || isUndefined;
return hasNotWon || isUndefined;
} else if (misc.val === "WIN" && misc.state === DropDownState.OFF) {
return true;
}
});
// HA Filter
const hasHA = starterData.abilityAttr & AbilityAttr.ABILITY_HIDDEN;
const fitsHA = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
if (misc.val === "HIDDEN_ABILITY" && misc.state === DropDownState.ON) {
return isHA;
return hasHA;
} else if (misc.val === "HIDDEN_ABILITY" && misc.state === DropDownState.EXCLUDE) {
return !isHA;
return !hasHA;
} else if (misc.val === "HIDDEN_ABILITY" && misc.state === DropDownState.OFF) {
return true;
}
});
// Egg Purchasable Filter
const isEggPurchasable = this.isSameSpeciesEggAvailable(container.species.speciesId);
const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
if (misc.val === "EGG" && misc.state === DropDownState.ON) {
return isEggPurchasable;
@ -2381,6 +2473,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
});
// Pokerus Filter
const fitsPokerus = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
if (misc.val === "POKERUS" && misc.state === DropDownState.ON) {
return this.pokerusSpecies.includes(container.species);
@ -2579,56 +2672,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.natureCursor = species ? this.scene.gameData.getSpeciesDefaultNature(species) : 0;
const starterAttributes : StarterAttributes | null = species ? {...this.starterPreferences[species.speciesId]} : null;
// validate starterAttributes
if (starterAttributes) {
// this may cause changes so we created a copy of the attributes before
if (starterAttributes.variant && !isNaN(starterAttributes.variant)) {
if (![
this.speciesStarterDexEntry!.caughtAttr & DexAttr.NON_SHINY, // TODO: is that bang correct?
this.speciesStarterDexEntry!.caughtAttr & DexAttr.DEFAULT_VARIANT, // TODO: is that bang correct?
this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_2, // TODO: is that bang correct?
this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_3 // TODO: is that bang correct?
][starterAttributes.variant+1]) { // add 1 as -1 = non-shiny
// requested variant wasn't unlocked, purging setting
delete starterAttributes.variant;
}
}
if (typeof starterAttributes.female !== "boolean" || !(starterAttributes.female ?
this.speciesStarterDexEntry!.caughtAttr & DexAttr.FEMALE : // TODO: is this bang correct?
this.speciesStarterDexEntry!.caughtAttr & DexAttr.MALE // TODO: is this bang correct?
)) {
// requested gender wasn't unlocked, purging setting
delete starterAttributes.female;
}
const abilityAttr = this.scene.gameData.starterData[species!.speciesId].abilityAttr; // TODO: is this bang correct?
if (![
abilityAttr & AbilityAttr.ABILITY_1,
species!.ability2 ? (abilityAttr & AbilityAttr.ABILITY_2) : abilityAttr & AbilityAttr.ABILITY_HIDDEN, // TODO: is this bang correct?
species!.ability2 && abilityAttr & AbilityAttr.ABILITY_HIDDEN // TODO: is this bang correct?
][starterAttributes.ability!]) { // TODO: is this bang correct?
// requested ability wasn't unlocked, purging setting
delete starterAttributes.ability;
}
if (!(species?.forms[starterAttributes.form!]?.isStarterSelectable && this.speciesStarterDexEntry!.caughtAttr & this.scene.gameData.getFormAttr(starterAttributes.form!))) { // TODO: are those bangs correct?
// requested form wasn't unlocked/isn't a starter form, purging setting
delete starterAttributes.form;
}
if (this.scene.gameData.getNaturesForAttr(this.speciesStarterDexEntry?.natureAttr).indexOf(starterAttributes.nature as unknown as Nature) < 0) {
// requested nature wasn't unlocked, purging setting
delete starterAttributes.nature;
}
}
if (starterAttributes?.nature) {
// load default nature from stater save data, if set
this.natureCursor = starterAttributes.nature;
}
if (starterAttributes?.ability && !isNaN(starterAttributes.ability)) {
// load default nature from stater save data, if set
// load default ability from stater save data, if set
this.abilityCursor = starterAttributes.ability;
}
@ -2675,7 +2725,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonLuckText.setText(luck.toString());
this.pokemonLuckText.setTint(getVariantTint(Math.min(luck - 1, 2) as Variant));
this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible);
this.pokemonShinyIcon.setVisible(this.starterPreferences[species.speciesId]?.shiny ?? false);
//Growth translate
let growthReadable = Utils.toReadableString(GrowthRate[species.growthRate]);
@ -2699,12 +2748,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species));
}
this.pokemonHatchedCountText.setText(`${this.speciesStarterDexEntry.hatchedCount}`);
const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
const defaultProps = this.scene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
const variant = defaultProps.variant;
const tint = getVariantTint(variant);
this.pokemonShinyIcon.setFrame(getVariantIcon(variant));
this.pokemonShinyIcon.setTint(tint);
this.pokemonShinyIcon.setVisible(defaultProps.shiny);
this.pokemonCaughtHatchedContainer.setVisible(true);
if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) {
this.pokemonCaughtHatchedContainer.setY(16);
@ -2894,21 +2945,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const dexEntry = this.scene.gameData.dexData[species.speciesId];
const abilityAttr = this.scene.gameData.starterData[species.speciesId].abilityAttr;
const isCaught = this.scene.gameData.dexData[species.speciesId]?.caughtAttr || BigInt(0);
const isVariant3Caught = !!(isCaught & DexAttr.VARIANT_3);
const isVariant2Caught = !!(isCaught & DexAttr.VARIANT_2);
const isDefaultVariantCaught = !!(isCaught & DexAttr.DEFAULT_VARIANT);
const isVariantCaught = !!(isCaught & DexAttr.SHINY);
const isMaleCaught = !!(isCaught & DexAttr.MALE);
const isFemaleCaught = !!(isCaught & DexAttr.FEMALE);
const starterAttributes = this.starterPreferences[species.speciesId];
const props = this.scene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId));
const defaultAbilityIndex = this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
const defaultNature = this.scene.gameData.getSpeciesDefaultNature(species);
const caughtAttr = this.scene.gameData.dexData[species.speciesId]?.caughtAttr || BigInt(0);
if (!dexEntry.caughtAttr) {
const props = this.scene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId));
const defaultAbilityIndex = this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
const defaultNature = this.scene.gameData.getSpeciesDefaultNature(species);
if (shiny === undefined || shiny !== props.shiny) {
shiny = props.shiny;
}
@ -2927,83 +2970,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
if (natureIndex === undefined || natureIndex !== defaultNature) {
natureIndex = defaultNature;
}
} else {
// compare current shiny, formIndex, female, variant, abilityIndex, natureIndex with the caught ones
// if the current ones are not caught, we need to find the next caught ones
if (shiny) {
if (!(isVariantCaught || isVariant2Caught || isVariant3Caught)) {
shiny = false;
starterAttributes.shiny = false;
variant = 0;
starterAttributes.variant = 0;
} else {
shiny = true;
starterAttributes.shiny = true;
if (variant === 0 && !isDefaultVariantCaught) {
if (isVariant2Caught) {
variant = 1;
starterAttributes.variant = 1;
} else if (isVariant3Caught) {
variant = 2;
starterAttributes.variant = 2;
} else {
variant = 0;
starterAttributes.variant = 0;
}
} else if (variant === 1 && !isVariant2Caught) {
if (isVariantCaught) {
variant = 0;
starterAttributes.variant = 0;
} else if (isVariant3Caught) {
variant = 2;
starterAttributes.variant = 2;
} else {
variant = 0;
starterAttributes.variant = 0;
}
} else if (variant === 2 && !isVariant3Caught) {
if (isVariantCaught) {
variant = 0;
starterAttributes.variant = 0;
} else if (isVariant2Caught) {
variant = 1;
starterAttributes.variant = 1;
} else {
variant = 0;
starterAttributes.variant = 0;
}
}
}
}
if (female) {
if (!isFemaleCaught) {
female = false;
starterAttributes.female = false;
}
} else {
if (!isMaleCaught) {
female = true;
starterAttributes.female = true;
}
}
if (species.forms) {
const formCount = species.forms.length;
let newFormIndex = formIndex??0;
if (species.forms[newFormIndex]) {
const isValidForm = species.forms[newFormIndex].isStarterSelectable && dexEntry.caughtAttr & this.scene.gameData.getFormAttr(newFormIndex);
if (!isValidForm) {
do {
newFormIndex = (newFormIndex + 1) % formCount;
if (species.forms[newFormIndex].isStarterSelectable && dexEntry.caughtAttr & this.scene.gameData.getFormAttr(newFormIndex)) {
break;
}
} while (newFormIndex !== props.formIndex);
formIndex = newFormIndex;
starterAttributes.form = formIndex;
}
}
}
}
this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default?
@ -3045,8 +3011,19 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
currentFilteredContainer.checkIconId(female, formIndex, shiny, variant);
}
this.canCycleShiny = isVariantCaught || isVariant2Caught || isVariant3Caught;
const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY);
const isShinyCaught = !!(caughtAttr & DexAttr.SHINY);
const isVariant1Caught = isShinyCaught && !!(caughtAttr & DexAttr.DEFAULT_VARIANT);
const isVariant2Caught = isShinyCaught && !!(caughtAttr & DexAttr.VARIANT_2);
const isVariant3Caught = isShinyCaught && !!(caughtAttr & DexAttr.VARIANT_3);
this.canCycleShiny = isNonShinyCaught && isShinyCaught;
this.canCycleVariant = !!shiny && [ isVariant1Caught, isVariant2Caught, isVariant3Caught].filter(v => v).length > 1;
const isMaleCaught = !!(caughtAttr & DexAttr.MALE);
const isFemaleCaught = !!(caughtAttr & DexAttr.FEMALE);
this.canCycleGender = isMaleCaught && isFemaleCaught;
const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1;
let hasAbility2 = abilityAttr & AbilityAttr.ABILITY_2;
const hasHiddenAbility = abilityAttr & AbilityAttr.ABILITY_HIDDEN;
@ -3061,10 +3038,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
this.canCycleAbility = [ hasAbility1, hasAbility2, hasHiddenAbility ].filter(a => a).length > 1;
this.canCycleForm = species.forms.filter(f => f.isStarterSelectable || !pokemonFormChanges[species.speciesId]?.find(fc => fc.formKey))
.map((_, f) => dexEntry.caughtAttr & this.scene.gameData.getFormAttr(f)).filter(f => f).length > 1;
this.canCycleNature = this.scene.gameData.getNaturesForAttr(dexEntry.natureAttr).length > 1;
this.canCycleVariant = !!shiny && [ dexEntry.caughtAttr & DexAttr.DEFAULT_VARIANT, dexEntry.caughtAttr & DexAttr.VARIANT_2, dexEntry.caughtAttr & DexAttr.VARIANT_3].filter(v => v).length > 1;
}
if (dexEntry.caughtAttr && species.malePercent !== null) {
@ -3442,39 +3420,55 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
return canStart;
}
/* this creates a temporary dex attr props that we use to check whether a pokemon is valid for a challenge.
* when checking for certain challenges (i.e. mono type), we need to check for form changes AND evolutions
* However, since some pokemon can evolve based on their intial gender/form, we need a way to look for that
* This temporary dex attr will therefore ONLY look at gender and form, since there's no cases of shinies/variants
* having different evolutions to their non shiny/variant part, and so those can be ignored
* Since the current form and gender is stored in the starter preferences, this is where we get the values from
*/
/**
* Creates a temporary dex attr props that will be used to check whether a pokemon is valid for a challenge
* and to display the correct shiny, variant, and form based on the StarterPreferences
*
* @param speciesId the id of the species to get props for
* @returns the dex props
*/
getCurrentDexProps(speciesId: number): bigint {
let props = 0n;
const caughtAttr = this.scene.gameData.dexData[speciesId].caughtAttr;
if (this.starterPreferences[speciesId]?.female) { // this checks the gender of the pokemon
/* this checks the gender of the pokemon; this works by checking a) that the starter preferences for the species exist, and if so, is it female. If so, it'll add DexAttr.FEMALE to our temp props
* It then checks b) if the caughtAttr for the pokemon is female and NOT male - this means that the ONLY gender we've gotten is female, and we need to add DexAttr.FEMALE to our temp props
* If neither of these pass, we add DexAttr.MALE to our temp props
*/
if (this.starterPreferences[speciesId]?.female || ((caughtAttr & DexAttr.FEMALE) > 0n && (caughtAttr & DexAttr.MALE) === 0n)) {
props += DexAttr.FEMALE;
} else {
props += DexAttr.MALE;
}
if (this.starterPreferences[speciesId]?.shiny) {
/* This part is very similar to above, but instead of for gender, it checks for shiny within starter preferences.
* If they're not there, it checks the caughtAttr for shiny only (i.e. SHINY === true && NON_SHINY === false)
*/
if (this.starterPreferences[speciesId]?.shiny || ((caughtAttr & DexAttr.SHINY) > 0n && (caughtAttr & DexAttr.NON_SHINY) === 0n)) {
props += DexAttr.SHINY;
if (this.starterPreferences[speciesId]?.variant) {
props += BigInt(Math.pow(2, this.starterPreferences[speciesId]?.variant)) * DexAttr.DEFAULT_VARIANT;
} else {
props += DexAttr.DEFAULT_VARIANT;
/* This calculates the correct variant if there's no starter preferences for it.
* This gets the lowest tier variant that you've caught (in line with other mechanics) and adds it to the temp props
*/
if ((caughtAttr & DexAttr.DEFAULT_VARIANT) > 0) {
props += DexAttr.DEFAULT_VARIANT;
}
if ((caughtAttr & DexAttr.VARIANT_2) > 0) {
props += DexAttr.VARIANT_2;
} else if ((caughtAttr & DexAttr.VARIANT_3) > 0) {
props += DexAttr.VARIANT_3;
}
}
} else {
props += DexAttr.NON_SHINY;
if (this.starterPreferences[speciesId]?.variant) {
delete this.starterPreferences[speciesId].variant;
}
props += DexAttr.DEFAULT_VARIANT; // we add the default variant here because non shiny versions are listed as default variant
}
if (this.starterPreferences[speciesId]?.form) { // this checks for the form of the pokemon
props += BigInt(Math.pow(2, this.starterPreferences[speciesId]?.form)) * DexAttr.DEFAULT_FORM;
} else {
props += DexAttr.DEFAULT_FORM;
// Get the first unlocked form
props += this.scene.gameData.getFormAttr(this.scene.gameData.getFormIndex(caughtAttr));
}
return props;

View File

@ -0,0 +1,147 @@
import { FormModalUiHandler } from "./form-modal-ui-handler";
import { ModalConfig } from "./modal-ui-handler";
import i18next from "i18next";
import { PlayerPokemon } from "#app/field/pokemon";
import { OptionSelectItem } from "./abstact-option-select-ui-handler";
import { isNullOrUndefined } from "#app/utils";
import { Mode } from "./ui";
export default class TestDialogueUiHandler extends FormModalUiHandler {
keys: string[];
constructor(scene, mode) {
super(scene, mode);
}
setup() {
super.setup();
const flattenKeys = (object, topKey?: string, midleKey?: string[]): Array<any> => {
return Object.keys(object).map((t, i) => {
const value = Object.values(object)[i];
if (typeof value === "object" && !isNullOrUndefined(value)) { // we check for not null or undefined here because if the language json file has a null key, the typeof will still be an object, but that object will be null, causing issues
// If the value is an object, execute the same process
// si el valor es un objeto ejecuta el mismo proceso
return flattenKeys(value, topKey ?? t, topKey ? midleKey ? [...midleKey, t] : [t] : undefined).filter((t) => t.length > 0);
} else if (typeof value === "string" || isNullOrUndefined(value)) { // we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key
// Return in the format expected by i18next
return midleKey ? `${topKey}:${midleKey.map((m) => m).join(".")}.${t}` : `${topKey}:${t}`;
}
}).filter((t) => t);
};
const keysInArrays = flattenKeys(i18next.getDataByLanguage(String(i18next.resolvedLanguage))).filter((t) => t.length > 0); // Array of arrays
const keys = keysInArrays.flat(Infinity).map(String); // One array of string
this.keys = keys;
}
getModalTitle(config?: ModalConfig): string {
return "Test Dialogue";
}
getFields(config?: ModalConfig): string[] {
return [ "Dialogue" ];
}
getWidth(config?: ModalConfig): number {
return 300;
}
getMargin(config?: ModalConfig): [number, number, number, number] {
return [ 0, 0, 48, 0 ];
}
getButtonLabels(config?: ModalConfig): string[] {
return [ "Check", "Cancel" ];
}
getReadableErrorMessage(error: string): string {
const colonIndex = error?.indexOf(":");
if (colonIndex > 0) {
error = error.slice(0, colonIndex);
}
return super.getReadableErrorMessage(error);
}
show(args: any[]): boolean {
const ui = this.getUi();
const input = this.inputs[0];
input.setMaxLength(255);
input.on("keydown", (inputObject, evt: KeyboardEvent) => {
if (["escape", "space"].some((v) => v === evt.key.toLowerCase() || v === evt.code.toLowerCase()) && ui.getMode() === Mode.AUTO_COMPLETE) {
// Delete autocomplete list and recovery focus.
inputObject.on("blur", () => inputObject.node.focus(), { once: true });
ui.revertMode();
}
});
input.on("textchange", (inputObject, evt: InputEvent) => {
// Delete autocomplete.
if (ui.getMode() === Mode.AUTO_COMPLETE) {
ui.revertMode();
}
let options: OptionSelectItem[] = [];
const splitArr = inputObject.text.split(" ");
const filteredKeys = this.keys.filter((command) => command.toLowerCase().includes(splitArr[splitArr.length - 1].toLowerCase()));
if (inputObject.text !== "" && filteredKeys.length > 0) {
// if performance is required, you could reduce the number of total results by changing the slice below to not have all ~8000 inputs going
options = filteredKeys.slice(0).map((value) => {
return {
label: value,
handler: () => {
// this is here to make sure that if you try to backspace then enter, the last known evt.data (backspace) is picked up
// this is because evt.data is null for backspace, so without this, the autocomplete windows just closes
if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") {
const separatedArray = inputObject.text.split(" ");
separatedArray[separatedArray.length - 1] = value;
inputObject.setText(separatedArray.join(" "));
}
ui.revertMode();
return true;
}
};
});
}
if (options.length > 0) {
const modalOpts = {
options: options,
maxOptions: 5,
modalContainer: this.modalContainer
};
ui.setOverlayMode(Mode.AUTO_COMPLETE, modalOpts);
}
});
if (super.show(args)) {
const config = args[0] as ModalConfig;
this.inputs[0].resize(1150, 116);
this.inputContainers[0].list[0].width = 200;
if (args[1] && typeof (args[1] as PlayerPokemon).getNameToRender === "function") {
this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender();
} else {
this.inputs[0].text = args[1];
}
this.submitAction = (_) => {
if (ui.getMode() === Mode.TEST_DIALOGUE) {
this.sanitizeInputs();
const sanitizedName = btoa(unescape(encodeURIComponent(this.inputs[0].text)));
config.buttonActions[0](sanitizedName);
return true;
}
return false;
};
return true;
}
return false;
}
}

View File

@ -49,6 +49,8 @@ import RenameFormUiHandler from "./rename-form-ui-handler";
import AdminUiHandler from "./admin-ui-handler";
import RunHistoryUiHandler from "./run-history-ui-handler";
import RunInfoUiHandler from "./run-info-ui-handler";
import TestDialogueUiHandler from "#app/ui/test-dialogue-ui-handler";
import AutoCompleteUiHandler from "./autocomplete-ui-handler";
export enum Mode {
MESSAGE,
@ -89,6 +91,8 @@ export enum Mode {
RENAME_POKEMON,
RUN_HISTORY,
RUN_INFO,
TEST_DIALOGUE,
AUTO_COMPLETE,
ADMIN,
}
@ -127,6 +131,8 @@ const noTransitionModes = [
Mode.UNAVAILABLE,
Mode.OUTDATED,
Mode.RENAME_POKEMON,
Mode.TEST_DIALOGUE,
Mode.AUTO_COMPLETE,
Mode.ADMIN,
];
@ -191,6 +197,8 @@ export default class UI extends Phaser.GameObjects.Container {
new RenameFormUiHandler(scene),
new RunHistoryUiHandler(scene),
new RunInfoUiHandler(scene),
new TestDialogueUiHandler(scene, Mode.TEST_DIALOGUE),
new AutoCompleteUiHandler(scene),
new AdminUiHandler(scene),
];
}