[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>
This commit is contained in:
Leo Kim 2024-08-30 03:59:33 +09:00 committed by GitHub
parent 1e95068f14
commit c112abbcd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 4005 additions and 3756 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

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

@ -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";
@ -37,6 +37,9 @@ import { StatChangePhase } from "#app/phases/stat-change-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms";
import { NumberHolder } from "#app/utils";
import { GameMode } from "#app/game-mode";
import { applyChallenges, ChallengeType } from "./challenge";
export enum MoveCategory {
PHYSICAL,
@ -4180,8 +4183,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;
@ -6203,7 +6210,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;
}
@ -6215,6 +6222,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();
@ -7940,7 +7966,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

@ -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"));
@ -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

@ -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

@ -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

@ -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

@ -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

@ -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.