diff --git a/.env.beta b/.env.beta new file mode 100644 index 00000000000..8d1e93b3277 --- /dev/null +++ b/.env.beta @@ -0,0 +1,3 @@ +VITE_BYPASS_LOGIN=0 +VITE_BYPASS_TUTORIAL=0 +VITE_SERVER_URL=https://api.beta.pokerogue.net \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 00000000000..74818d41a12 --- /dev/null +++ b/.env.production @@ -0,0 +1,3 @@ +VITE_BYPASS_LOGIN=0 +VITE_BYPASS_TUTORIAL=0 +VITE_SERVER_URL=https://api.pokerogue.net \ No newline at end of file diff --git a/.github/workflows/deploy-beta.yml b/.github/workflows/deploy-beta.yml new file mode 100644 index 00000000000..d954d9bb865 --- /dev/null +++ b/.github/workflows/deploy-beta.yml @@ -0,0 +1,33 @@ +name: Deploy Beta + +on: + push: {} + pull_request: {} + +jobs: + deploy: + if: github.repository == 'pagefaultgames/pokerogue' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Install dependencies + run: npm ci + - name: Build + run: npm run build:beta + env: + NODE_ENV: production + - name: Set up SSH + if: github.event_name == 'push' && github.ref_name == github.event.repository.default_branch + run: | + mkdir ~/.ssh + echo "${{ secrets.BETA_SSH_PUBLIC_KEY }}" > ~/.ssh/id_ed25519.pub + echo "${{ secrets.BETA_SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/* + ssh-keyscan -H ${{ secrets.BETA_SSH_HOST }} >> ~/.ssh/known_hosts + - name: Deploy build on server + if: github.event_name == 'push' && github.ref_name == github.event.repository.default_branch + run: | + rsync --del --no-times --checksum -vrm dist/* ${{ secrets.BETA_SSH_USER }}@${{ secrets.BETA_SSH_HOST }}:${{ secrets.BETA_DESTINATION_DIR }} \ No newline at end of file diff --git a/package.json b/package.json index 29a956f19c6..160ca965bc8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "start": "vite", "start:dev": "vite --mode development", "build": "vite build", + "build:beta": "vite build --mode beta", "preview": "vite preview", "test": "vitest run", "test:cov": "vitest run --coverage", diff --git a/public/images/pokemon/variant/_masterlist.json b/public/images/pokemon/variant/_masterlist.json index 0c48fed625b..f3e690395e1 100644 --- a/public/images/pokemon/variant/_masterlist.json +++ b/public/images/pokemon/variant/_masterlist.json @@ -3265,6 +3265,11 @@ 1, 1 ], + "123": [ + 1, + 1, + 1 + ], "129": [ 0, 1, @@ -6638,6 +6643,11 @@ 1, 1 ], + "123": [ + 1, + 1, + 1 + ], "129": [ 0, 1, diff --git a/public/images/pokemon/variant/back/female/123.json b/public/images/pokemon/variant/back/female/123.json new file mode 100644 index 00000000000..049e6e23435 --- /dev/null +++ b/public/images/pokemon/variant/back/female/123.json @@ -0,0 +1,47 @@ +{ + "0": { + "425a21": "632929", + "bde673": "e67373", + "e6d6ad": "b5b5ce", + "9c8c31": "632929", + "8cce73": "f76b6b", + "101010": "101010", + "fff7d6": "ffffff", + "5a9c4a": "d63a3a", + "bdbdbd": "bdbdbd", + "c5a573": "b5b5ce", + "dedede": "dedede", + "ffffff": "ffffff", + "737373": "737373" + }, + "1": { + "425a21": "484e75", + "bde673": "bdbdbd", + "e6d6ad": "e6d6ad", + "9c8c31": "9c8c31", + "8cce73": "92b0db", + "101010": "101010", + "fff7d6": "fff7d6", + "5a9c4a": "7b94d6", + "bdbdbd": "ffffff", + "c5a573": "9cc5ff", + "dedede": "dedede", + "ffffff": "ffffff", + "737373": "737373" + }, + "2": { + "425a21": "8f3907", + "bde673": "f8f581", + "e6d6ad": "e6d6ad", + "9c8c31": "9c8c31", + "8cce73": "f0c947", + "101010": "101010", + "fff7d6": "fff7d6", + "5a9c4a": "e6a027", + "bdbdbd": "bdbdbd", + "c5a573": "c5a573", + "dedede": "dedede", + "ffffff": "ffffff", + "737373": "737373" + } +} \ No newline at end of file diff --git a/public/images/pokemon/variant/female/123.json b/public/images/pokemon/variant/female/123.json new file mode 100644 index 00000000000..5fbefd72224 --- /dev/null +++ b/public/images/pokemon/variant/female/123.json @@ -0,0 +1,44 @@ +{ + "0": { + "425a21": "632929", + "bde673": "f76b6b", + "101010": "101010", + "9c8c31": "9494a5", + "fff7d6": "ffffff", + "8cce73": "d63a3a", + "e6d6ad": "b5b5ce", + "5a9c4a": "a52929", + "ffffff": "ffffff", + "dedede": "dedede", + "bdbdbd": "bdbdbd", + "737373": "737373" + }, + "1": { + "425a21": "484e75", + "bde673": "7c9ac5", + "101010": "101010", + "9c8c31": "9c8c31", + "fff7d6": "fff7d6", + "8cce73": "92b0db", + "e6d6ad": "e6d6ad", + "5a9c4a": "7b94d6", + "ffffff": "ffffff", + "dedede": "dedede", + "bdbdbd": "bdbdbd", + "737373": "737373" + }, + "2": { + "425a21": "8f3907", + "bde673": "f8f581", + "101010": "101010", + "9c8c31": "9c8c31", + "fff7d6": "fff7d6", + "8cce73": "f0c947", + "e6d6ad": "e6d6ad", + "5a9c4a": "e6a027", + "ffffff": "ffffff", + "dedede": "f0c947", + "bdbdbd": "bdbdbd", + "737373": "737373" + } +} \ No newline at end of file diff --git a/src/data/ability.ts b/src/data/ability.ts index eb55f661e66..4f4807ed614 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1446,7 +1446,7 @@ export class FieldMovePowerBoostAbAttr extends AbAttr { * Boosts the power of a specific type of move. * @extends FieldMovePowerBoostAbAttr */ -export class FieldMoveTypePowerBoostAbAttr extends FieldMovePowerBoostAbAttr { +export class PreAttackFieldMoveTypePowerBoostAbAttr extends FieldMovePowerBoostAbAttr { /** * @param boostedType - The type of move that will receive the power boost. * @param powerMultiplier - The multiplier to apply to the move's power, defaults to 1.5 if not provided. @@ -1456,6 +1456,18 @@ export class FieldMoveTypePowerBoostAbAttr extends FieldMovePowerBoostAbAttr { } } +/** + * Boosts the power of a specific type of move for all Pokemon in the field. + * @extends PreAttackFieldMoveTypePowerBoostAbAttr + */ +export class FieldMoveTypePowerBoostAbAttr extends PreAttackFieldMoveTypePowerBoostAbAttr { } + +/** + * Boosts the power of a specific type of move for the user and its allies. + * @extends PreAttackFieldMoveTypePowerBoostAbAttr + */ +export class UserFieldMoveTypePowerBoostAbAttr extends PreAttackFieldMoveTypePowerBoostAbAttr { } + /** * Boosts the power of moves in specified categories. * @extends FieldMovePowerBoostAbAttr @@ -4964,8 +4976,7 @@ export function initAbilities() { new Ability(Abilities.SCREEN_CLEANER, 8) .attr(PostSummonRemoveArenaTagAbAttr, [ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT]), new Ability(Abilities.STEELY_SPIRIT, 8) - .attr(MoveTypePowerBoostAbAttr, Type.STEEL) - .partial(), + .attr(UserFieldMoveTypePowerBoostAbAttr, Type.STEEL), new Ability(Abilities.PERISH_BODY, 8) .attr(PostDefendPerishSongAbAttr, 4), new Ability(Abilities.WANDERING_SPIRIT, 8) diff --git a/src/data/pokemon-level-moves.ts b/src/data/pokemon-level-moves.ts index 5f01b4db02f..9f1f95e42ff 100644 --- a/src/data/pokemon-level-moves.ts +++ b/src/data/pokemon-level-moves.ts @@ -3488,6 +3488,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 0, Moves.DESTINY_BOND ], [ 0, Moves.SAFEGUARD ], [ 0, Moves.MIRROR_COAT ], + [ 1, Moves.COUNTER ], + [ 1, Moves.DESTINY_BOND ], + [ 1, Moves.SAFEGUARD ], + [ 1, Moves.MIRROR_COAT ], [ 1, Moves.AMNESIA ], [ 1, Moves.SPLASH ], [ 1, Moves.CHARM ], @@ -5827,20 +5831,20 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 56, Moves.SKY_ATTACK ], ], [Species.ZANGOOSE]: [ + [ -1, Moves.DOUBLE_KICK ], + [ -1, Moves.DISABLE ], + [ -1, Moves.COUNTER ], + [ -1, Moves.FURY_SWIPES ], + [ -1, Moves.CURSE ], + [ -1, Moves.FLAIL ], + [ -1, Moves.BELLY_DRUM ], + [ -1, Moves.FEINT ], + [ -1, Moves.NIGHT_SLASH ], + [ -1, Moves.DOUBLE_HIT ], + [ -1, Moves.QUICK_GUARD ], + [ -1, Moves.FINAL_GAMBIT ], [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], - [ 1, Moves.DOUBLE_KICK ], - [ 1, Moves.DISABLE ], - [ 1, Moves.COUNTER ], - [ 1, Moves.FURY_SWIPES ], - [ 1, Moves.CURSE ], - [ 1, Moves.FLAIL ], - [ 1, Moves.BELLY_DRUM ], - [ 1, Moves.FEINT ], - [ 1, Moves.NIGHT_SLASH ], - [ 1, Moves.DOUBLE_HIT ], - [ 1, Moves.QUICK_GUARD ], - [ 1, Moves.FINAL_GAMBIT ], [ 5, Moves.QUICK_ATTACK ], [ 8, Moves.FURY_CUTTER ], [ 12, Moves.METAL_CLAW ], @@ -14125,6 +14129,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 0, Moves.CONFUSION ], [ 0, Moves.LIGHT_SCREEN ], [ 0, Moves.REFLECT ], + [ 1, Moves.CONFUSION ], + [ 1, Moves.LIGHT_SCREEN ], + [ 1, Moves.REFLECT ], [ 1, Moves.STRUGGLE_BUG ], ], [Species.ORBEETLE]: [ @@ -17169,10 +17176,12 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 98, Moves.HYPER_BEAM ], ], [Species.WALKING_WAKE]: [ - [ 0, Moves.LEER ], - [ 0, Moves.ROAR ], - [ 0, Moves.TWISTER ], - [ 0, Moves.AQUA_JET ], + [ -1, Moves.SUNNY_DAY ], + [ -1, Moves.HONE_CLAWS ], + [ 1, Moves.LEER ], + [ 1, Moves.ROAR ], + [ 1, Moves.TWISTER ], + [ 1, Moves.AQUA_JET ], [ 7, Moves.BITE ], [ 14, Moves.WATER_PULSE ], [ 21, Moves.NOBLE_ROAR ], @@ -17186,10 +17195,12 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 84, Moves.HYDRO_PUMP ], ], [Species.IRON_LEAVES]: [ - [ 0, Moves.LEER ], - [ 0, Moves.QUICK_ATTACK ], - [ 0, Moves.HELPING_HAND ], - [ 0, Moves.WORK_UP ], + [ -1, Moves.ELECTRIC_TERRAIN ], + [ -1, Moves.QUASH ], + [ 1, Moves.LEER ], + [ 1, Moves.QUICK_ATTACK ], + [ 1, Moves.HELPING_HAND ], + [ 1, Moves.WORK_UP ], [ 7, Moves.MAGICAL_LEAF ], [ 14, Moves.RETALIATE ], [ 21, Moves.QUICK_GUARD ], @@ -17353,6 +17364,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 54, Moves.POWER_WHIP ], ], [Species.GOUGING_FIRE]: [ + [ -1, Moves.DOUBLE_KICK ], + [ -1, Moves.ANCIENT_POWER ], + [ -1, Moves.NOBLE_ROAR ], [ 1, Moves.STOMP ], [ 1, Moves.LEER ], [ 1, Moves.INCINERATE ], @@ -17372,6 +17386,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 91, Moves.RAGING_FURY ], ], [Species.RAGING_BOLT]: [ + [ -1, Moves.ANCIENT_POWER ], [ 1, Moves.TWISTER ], [ 1, Moves.SUNNY_DAY ], [ 1, Moves.SHOCK_WAVE ], diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 962772d6e78..2b488f330c4 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -779,7 +779,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali } // This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon - getSimulatedEvolutionChain(currentLevel: integer, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false) { + getSimulatedEvolutionChain(currentLevel: integer, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): [Species, integer][] { const ret = []; if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) { const prevolutionLevels = this.getPrevolutionLevels().reverse(); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 88d7f6b73a8..b76717d7d44 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -23,7 +23,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag"; -import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr } from "../data/ability"; +import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr, UserFieldMoveTypePowerBoostAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -849,8 +849,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return ret; } + /** + * All moves that could be relearned by this pokemon at this point. Used for memory mushrooms. + * @returns {Moves[]} The valid moves + */ getLearnableLevelMoves(): Moves[] { - return this.getLevelMoves(1, true).map(lm => lm[1]).filter(lm => !this.moveset.filter(m => m.moveId === lm).length).filter((move: Moves, i: integer, array: Moves[]) => array.indexOf(move) === i); + return this.getLevelMoves(1, true, false, true).map(lm => lm[1]).filter(lm => !this.moveset.filter(m => m.moveId === lm).length).filter((move: Moves, i: integer, array: Moves[]) => array.indexOf(move) === i); } /** @@ -1235,7 +1239,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return null; } - getLevelMoves(startingLevel?: integer, includeEvolutionMoves: boolean = false, simulateEvolutionChain: boolean = false): LevelMoves { + /** + * Gets all level up moves in a given range for a particular pokemon. + * @param {integer} startingLevel Don't include moves below this level + * @param {boolean} includeEvolutionMoves Whether to include evolution moves + * @param {boolean} simulateEvolutionChain Whether to include moves from prior evolutions + * @param {boolean} includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves + * @returns {LevelMoves} A list of moves and the levels they can be learned at + */ + getLevelMoves(startingLevel?: integer, includeEvolutionMoves: boolean = false, simulateEvolutionChain: boolean = false, includeRelearnerMoves: boolean = false): LevelMoves { const ret: LevelMoves = []; let levelMoves: LevelMoves = []; if (!startingLevel) { @@ -1246,62 +1258,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { for (let e = 0; e < evolutionChain.length; e++) { // TODO: Might need to pass specific form index in simulated evolution chain const speciesLevelMoves = getPokemonSpeciesForm(evolutionChain[e][0] as Species, this.formIndex).getLevelMoves(); - levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && !lm[0]) || ((!e || lm[0] > 1) && (e === evolutionChain.length - 1 || lm[0] <= evolutionChain[e + 1][1])))); - } - levelMoves.sort((lma: [integer, integer], lmb: [integer, integer]) => lma[0] > lmb[0] ? 1 : lma[0] < lmb[0] ? -1 : 0); - const uniqueMoves: Moves[] = []; - levelMoves = levelMoves.filter(lm => { - if (uniqueMoves.find(m => m === lm[1])) { - return false; + if (includeRelearnerMoves) { + levelMoves.push(...speciesLevelMoves); + } else { + levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === evolutionChain.length - 1 || lm[0] <= evolutionChain[e + 1][1])))); } - uniqueMoves.push(lm[1]); - return true; - }); + } } else { - levelMoves = this.getSpeciesForm(true).getLevelMoves(); + levelMoves = this.getSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0); } if (this.fusionSpecies) { - const evolutionLevelMoves = levelMoves.slice(0, Math.max(levelMoves.findIndex(lm => !!lm[0]), 0)); - const fusionLevelMoves = this.getFusionSpeciesForm(true).getLevelMoves(); - const fusionEvolutionLevelMoves = fusionLevelMoves.slice(0, Math.max(fusionLevelMoves.findIndex(flm => !!flm[0]), 0)); - const newLevelMoves: LevelMoves = []; - while (levelMoves.length && levelMoves[0][0] < startingLevel) { - levelMoves.shift(); - } - while (fusionLevelMoves.length && fusionLevelMoves[0][0] < startingLevel) { - fusionLevelMoves.shift(); - } - if (includeEvolutionMoves) { - for (const elm of evolutionLevelMoves.reverse()) { - levelMoves.unshift(elm); - } - for (const felm of fusionEvolutionLevelMoves.reverse()) { - fusionLevelMoves.unshift(felm); - } - } - for (let l = includeEvolutionMoves ? 0 : startingLevel; l <= this.level; l++) { - if (l === 1 && startingLevel > 1) { - l = startingLevel; - } - while (levelMoves.length && levelMoves[0][0] === l) { - const levelMove = levelMoves.shift(); - if (!newLevelMoves.find(lm => lm[1] === levelMove[1])) { - newLevelMoves.push(levelMove); - } - } - while (fusionLevelMoves.length && fusionLevelMoves[0][0] === l) { - const fusionLevelMove = fusionLevelMoves.shift(); - if (!newLevelMoves.find(lm => lm[1] === fusionLevelMove[1])) { - newLevelMoves.push(fusionLevelMove); + if (simulateEvolutionChain) { + const fusionEvolutionChain = this.fusionSpecies.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer()); + for (let e = 0; e < fusionEvolutionChain.length; e++) { + // TODO: Might need to pass specific form index in simulated evolution chain + const speciesLevelMoves = getPokemonSpeciesForm(fusionEvolutionChain[e][0] as Species, this.fusionFormIndex).getLevelMoves(); + if (includeRelearnerMoves) { + levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || lm[0] !== 0)); + } else { + levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === fusionEvolutionChain.length - 1 || lm[0] <= fusionEvolutionChain[e + 1][1])))); } } + } else { + levelMoves.push(...this.getFusionSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0)); } - levelMoves = newLevelMoves; } + levelMoves.sort((lma: [integer, integer], lmb: [integer, integer]) => lma[0] > lmb[0] ? 1 : lma[0] < lmb[0] ? -1 : 0); + const uniqueMoves: Moves[] = []; + levelMoves = levelMoves.filter(lm => { + if (uniqueMoves.find(m => m === lm[1])) { + return false; + } + uniqueMoves.push(lm[1]); + return true; + }); + if (levelMoves) { for (const lm of levelMoves) { const level = lm[0]; - if ((!includeEvolutionMoves || level) && level < startingLevel) { + if (!includeRelearnerMoves && ((level > 0 && level < startingLevel) || (!includeEvolutionMoves && level === 0) || level < 0)) { continue; } else if (level > this.level) { break; @@ -1788,6 +1783,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { aura.applyPreAttack(null, null, null, move, [power]); } + const alliedField: Pokemon[] = source instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField(); + alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, this, move, power)); + power.value *= typeChangeMovePowerMultiplier.value; if (!typeless) { @@ -3114,7 +3112,7 @@ export class PlayerPokemon extends Pokemon { fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null ].filter(d => d); const amount = new Utils.IntegerHolder(friendship); - const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic ? 2 : 1) / (fusionStarterSpeciesId ? 2 : 1))); + const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? 2 : 1) / (fusionStarterSpeciesId ? 2 : 1))); if (amount.value > 0) { this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index f7c23406179..0b339906cc5 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2116,14 +2116,38 @@ export class SwitchEffectTransferModifier extends PokemonHeldItemModifier { } } +/** + * Abstract class for held items that steal other Pokemon's items. + * @see {@linkcode TurnHeldItemTransferModifier} + * @see {@linkcode ContactHeldItemTransferChanceModifier} + */ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); } + /** + * Determines the targets to transfer items from when this applies. + * @param args\[0\] the {@linkcode Pokemon} holding this item + * @returns the opponents of the source {@linkcode Pokemon} + */ + getTargets(args: any[]): Pokemon[] { + const pokemon = args[0]; + + return pokemon instanceof Pokemon + ? pokemon.getOpponents() + : []; + } + + /** + * Steals an item from a set of target Pokemon. + * This prioritizes high-tier held items when selecting the item to steal. + * @param args \[0\] The {@linkcode Pokemon} holding this item + * @returns true if an item was stolen; false otherwise. + */ apply(args: any[]): boolean { const pokemon = args[0] as Pokemon; - const opponents = pokemon.getOpponents(); + const opponents = this.getTargets(args); if (!opponents.length) { return false; @@ -2180,6 +2204,11 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { abstract getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierTypes.ModifierType): string; } +/** + * Modifier for held items that steal items from the enemy at the end of + * each turn. + * @see {@linkcode modifierTypes[MINI_BLACK_HOLE]} + */ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -2210,6 +2239,12 @@ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { } } +/** + * Modifier for held items that add a chance to steal items from the target of a + * successful attack. + * @see {@linkcode modifierTypes[GRIP_CLAW]} + * @see {@linkcode HeldItemTransferModifier} + */ export class ContactHeldItemTransferChanceModifier extends HeldItemTransferModifier { private chance: number; @@ -2219,6 +2254,20 @@ export class ContactHeldItemTransferChanceModifier extends HeldItemTransferModif this.chance = chancePercent / 100; } + /** + * Determines the target to steal items from when this applies. + * @param args\[0\] The {@linkcode Pokemon} holding this item + * @param args\[1\] The {@linkcode Pokemon} the holder is targeting with an attack + * @returns The target (args[1]) stored in array format for use in {@linkcode HeldItemTransferModifier.apply} + */ + getTargets(args: any[]): Pokemon[] { + const target = args[1]; + + return target instanceof Pokemon + ? [ target ] + : []; + } + matchType(modifier: Modifier): boolean { return modifier instanceof ContactHeldItemTransferChanceModifier; } diff --git a/src/phases.ts b/src/phases.ts index 9f069a60dc5..df7314e6285 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2966,7 +2966,7 @@ export class MoveEffectPhase extends PokemonPhase { })).then(() => { applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { if (this.move.getMove() instanceof AttackMove) { - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target.getFieldIndex()); + this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); } resolve(); }); @@ -3095,7 +3095,7 @@ export class MoveEffectPhase extends PokemonPhase { applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, user, BattleStat.ACC, accuracyMultiplier, this.move.getMove()); const evasionMultiplier = new Utils.NumberHolder(1); - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this.getTarget(), BattleStat.EVA, evasionMultiplier); + applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier); accuracyMultiplier.value /= evasionMultiplier.value; diff --git a/src/test/abilities/sand_veil.test.ts b/src/test/abilities/sand_veil.test.ts new file mode 100644 index 00000000000..221d48d3ca1 --- /dev/null +++ b/src/test/abilities/sand_veil.test.ts @@ -0,0 +1,83 @@ +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import GameManager from "../utils/gameManager"; +import * as Overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "../utils/gameManagerUtils"; +import { CommandPhase, MoveEffectPhase, MoveEndPhase } from "#app/phases.js"; +import { BattleStat } from "#app/data/battle-stat.js"; +import { WeatherType } from "#app/data/weather.js"; +import { BattleStatMultiplierAbAttr, allAbilities } from "#app/data/ability.js"; + +const TIMEOUT = 20 * 1000; + +describe("Abilities - Sand Veil", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(Overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]); + vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MEOWSCARADA); + vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA); + vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TWISTER, Moves.TWISTER, Moves.TWISTER, Moves.TWISTER]); + vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(WeatherType.SANDSTORM); + }); + + test( + "ability should increase the evasiveness of the source", + async () => { + await game.startBattle([Species.SNORLAX, Species.BLISSEY]); + + const leadPokemon = game.scene.getPlayerField(); + leadPokemon.forEach(p => expect(p).toBeDefined()); + + const enemyPokemon = game.scene.getEnemyField(); + enemyPokemon.forEach(p => expect(p).toBeDefined()); + + vi.spyOn(leadPokemon[0], "getAbility").mockReturnValue(allAbilities[Abilities.SAND_VEIL]); + + const sandVeilAttr = allAbilities[Abilities.SAND_VEIL].getAttrs(BattleStatMultiplierAbAttr)[0]; + vi.spyOn(sandVeilAttr, "applyBattleStat").mockImplementation( + (pokemon, passive, battleStat, statValue, args) => { + if (battleStat === BattleStat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) { + statValue.value *= -1; // will make all attacks miss + return true; + } + return false; + } + ); + + expect(leadPokemon[0].hasAbility(Abilities.SAND_VEIL)).toBe(true); + expect(leadPokemon[1].hasAbility(Abilities.SAND_VEIL)).toBe(false); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + + await game.phaseInterceptor.to(CommandPhase); + + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + await game.phaseInterceptor.to(MoveEndPhase, false); + + expect(leadPokemon[0].hp).toBe(leadPokemon[0].getMaxHp()); + expect(leadPokemon[1].hp).toBeLessThan(leadPokemon[1].getMaxHp()); + }, TIMEOUT + ); +}); diff --git a/src/test/abilities/steely_spirit.test.ts b/src/test/abilities/steely_spirit.test.ts new file mode 100644 index 00000000000..e9c673a102a --- /dev/null +++ b/src/test/abilities/steely_spirit.test.ts @@ -0,0 +1,115 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon.js"; +import Move, { allMoves } from "#app/data/move.js"; +import { NumberHolder } from "#app/utils.js"; +import { allAbilities, applyPreAttackAbAttrs, UserFieldMoveTypePowerBoostAbAttr } from "#app/data/ability.js"; +import { Abilities } from "#app/enums/abilities.js"; + +describe("Abilities - Steely Spirit", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const steelySpiritMultiplier = 1.5; + const moveToCheck = Moves.IRON_HEAD; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.IRON_HEAD, Moves.SPLASH]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + }); + + it("increases Steel-type moves used by the user and its allies", async () => { + await game.startBattle([Species.MAGIKARP, Species.PERRSERKER]); + const perserrker = game.scene.getPlayerField()[1]; + + vi.spyOn(perserrker, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); + + expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, moveToCheck)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), perserrker, allMoves[moveToCheck]); + + expect(mockedMovePower).toBe(allMoves[moveToCheck].power * steelySpiritMultiplier); + }); + + it("stacks if multiple users with this ability are on the field.", async () => { + await game.startBattle([Species.PERRSERKER, Species.PERRSERKER]); + + game.scene.getPlayerField().forEach(p => { + vi.spyOn(p, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); + }); + + expect(game.scene.getPlayerField().every(p => p.hasAbility(Abilities.STEELY_SPIRIT))).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, moveToCheck)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToCheck]); + + expect(mockedMovePower).toBe(allMoves[moveToCheck].power * Math.pow(steelySpiritMultiplier, 2)); + }); + + it("does not take effect when suppressed", async () => { + await game.startBattle([Species.MAGIKARP, Species.PERRSERKER]); + const perserrker = game.scene.getPlayerField()[1]; + + vi.spyOn(perserrker, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]); + expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true); + + perserrker.summonData.abilitySuppressed = true; + + expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(false); + expect(perserrker.summonData.abilitySuppressed).toBe(true); + + game.doAttack(getMovePosition(game.scene, 0, moveToCheck)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), perserrker, allMoves[moveToCheck]); + + expect(mockedMovePower).toBe(allMoves[moveToCheck].power); + }); +}); + +/** + * Calculates the mocked power of a move. + * Note this does not consider other damage calculations + * except the power multiplier from Steely Spirit. + * + * @param defender - The defending Pokémon. + * @param attacker - The attacking Pokémon. + * @param move - The move being used by the attacker. + * @returns The adjusted power of the move. + */ +const getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => { + const powerHolder = new NumberHolder(move.power); + + /** + * Check if pokemon has the specified ability and is in effect. + * See Pokemon.hasAbility {@linkcode Pokemon.hasAbility} + */ + if (attacker.hasAbility(Abilities.STEELY_SPIRIT)) { + const alliedField: Pokemon[] = attacker instanceof PlayerPokemon ? attacker.scene.getPlayerField() : attacker.scene.getEnemyField(); + alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, this, move, powerHolder)); + } + + return powerHolder.value; +}; diff --git a/src/test/items/grip_claw.test.ts b/src/test/items/grip_claw.test.ts new file mode 100644 index 00000000000..ae621770da6 --- /dev/null +++ b/src/test/items/grip_claw.test.ts @@ -0,0 +1,75 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phase from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { Moves } from "#app/enums/moves.js"; +import { Species } from "#app/enums/species.js"; +import { BerryType } from "#app/enums/berry-type.js"; +import { Abilities } from "#app/enums/abilities.js"; +import { getMovePosition } from "../utils/gameManagerUtils"; +import { CommandPhase, MoveEndPhase, SelectTargetPhase } from "#app/phases.js"; +import { BattlerIndex } from "#app/battle.js"; +import { allMoves } from "#app/data/move.js"; + +const TIMEOUT = 20 * 1000; // 20 seconds + +describe("Items - Grip Claw", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phase.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.POPULATION_BOMB, Moves.SPLASH ]); + vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "GRIP_CLAW", count: 5}, {name: "MULTI_LENS", count: 3}]); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.KLUTZ); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH ]); + vi.spyOn(overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([ + {name: "BERRY", type: BerryType.SITRUS, count: 2}, + {name: "BERRY", type: BerryType.LUM, count: 2} + ]); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + + vi.spyOn(allMoves[Moves.POPULATION_BOMB], "accuracy", "get").mockReturnValue(100); + }); + + it( + "should only steal items from the attack target", + async () => { + await game.startBattle([Species.PANSEAR, Species.ROWLET, Species.PANPOUR, Species.PANSAGE, Species.CHARMANDER, Species.SQUIRTLE]); + + const playerPokemon = game.scene.getPlayerField(); + playerPokemon.forEach(p => expect(p).toBeDefined()); + + const enemyPokemon = game.scene.getEnemyField(); + enemyPokemon.forEach(p => expect(p).toBeDefined()); + + const enemyHeldItemCt = enemyPokemon.map(p => p.getHeldItems.length); + + game.doAttack(getMovePosition(game.scene, 0, Moves.POPULATION_BOMB)); + + await game.phaseInterceptor.to(SelectTargetPhase, false); + game.doSelectTarget(BattlerIndex.ENEMY); + + await game.phaseInterceptor.to(CommandPhase, false); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + await game.phaseInterceptor.to(MoveEndPhase, false); + + expect(enemyPokemon[1].getHeldItems.length).toBe(enemyHeldItemCt[1]); + }, TIMEOUT + ); +}); diff --git a/src/test/lokalisation/status-effect.test.ts b/src/test/lokalisation/status-effect.test.ts index 4c79dacbff7..b4267ea7b8b 100644 --- a/src/test/lokalisation/status-effect.test.ts +++ b/src/test/lokalisation/status-effect.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { beforeAll, describe, afterEach, expect, it, vi } from "vitest"; import { StatusEffect, getStatusEffectActivationText, @@ -8,7 +8,6 @@ import { getStatusEffectOverlapText, } from "#app/data/status-effect"; import i18next, { ParseKeys } from "i18next"; -import { afterEach } from "node:test"; const tMock = (key: ParseKeys) => key; const pokemonName = "PKM"; diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 687f65281b4..d0e644870a1 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -2366,7 +2366,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } else { levelMoves = pokemonSpeciesLevelMoves[species.speciesId]; } - this.speciesStarterMoves.push(...levelMoves.filter(lm => lm[0] <= 5).map(lm => lm[1])); + this.speciesStarterMoves.push(...levelMoves.filter(lm => lm[0] > 0 && lm[0] <= 5).map(lm => lm[1])); if (speciesEggMoves.hasOwnProperty(species.speciesId)) { for (let em = 0; em < 4; em++) { if (this.scene.gameData.starterData[species.speciesId].eggMoves & Math.pow(2, em)) { diff --git a/src/utils.ts b/src/utils.ts index 5a67df314d2..5aa558bae3a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -288,8 +288,7 @@ export const isLocal = ( export const localServerUrl = import.meta.env.VITE_SERVER_URL ?? `http://${window.location.hostname}:${window.location.port+1}`; // Set the server URL based on whether it's local or not -export const serverUrl = isLocal ? localServerUrl : ""; -export const apiUrl = isLocal ? serverUrl : "https://api.pokerogue.net"; +export const apiUrl = localServerUrl ?? "https://api.pokerogue.net"; // used to disable api calls when isLocal is true and a server is not found export let isLocalServerConnected = true;