Merge branch 'beta' into hebrew-pr

This commit is contained in:
Lugiad 2024-09-22 03:57:17 +02:00 committed by GitHub
commit d0f6dd7df7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 573 additions and 39 deletions

View File

@ -1146,6 +1146,13 @@ export default class BattleScene extends SceneBase {
}
}
getDoubleBattleChance(newWaveIndex: number, playerField: PlayerPokemon[]) {
const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8);
this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance);
playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, false, doubleChance));
return Math.max(doubleChance.value, 1);
}
newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean, mysteryEncounterType?: MysteryEncounterType): Battle | null {
const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave;
const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (_startingWave - 1)) + 1);
@ -1229,10 +1236,7 @@ export default class BattleScene extends SceneBase {
if (double === undefined && newWaveIndex > 1) {
if (newBattleType === BattleType.WILD && !this.gameMode.isWaveFinal(newWaveIndex)) {
const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8);
this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance);
playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, false, doubleChance));
newDouble = !Utils.randSeedInt(doubleChance.value);
newDouble = !Utils.randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
} else if (newBattleType === BattleType.TRAINER) {
newDouble = newTrainer?.variant === TrainerVariant.DOUBLE;
}

View File

@ -165,14 +165,27 @@ export class BlockRecoilDamageAttr extends AbAttr {
}
}
/**
* Attribute for abilities that increase the chance of a double battle
* occurring.
* @see apply
*/
export class DoubleBattleChanceAbAttr extends AbAttr {
constructor() {
super(false);
}
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const doubleChance = (args[0] as Utils.IntegerHolder);
doubleChance.value = Math.max(doubleChance.value / 2, 1);
/**
* Increases the chance of a double battle occurring
* @param args [0] {@linkcode Utils.NumberHolder} for double battle chance
* @returns true if the ability was applied
*/
apply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]): boolean {
const doubleBattleChance = args[0] as Utils.NumberHolder;
// This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using Utils.randSeedInt
// A double battle will initiate if the generated number is 0
doubleBattleChance.value = doubleBattleChance.value / 4;
return true;
}
}
@ -4831,11 +4844,9 @@ export function initAbilities() {
.bypassFaint(),
new Ability(Abilities.VOLT_ABSORB, 3)
.attr(TypeImmunityHealAbAttr, Type.ELECTRIC)
.partial() // Healing not blocked by Heal Block
.ignorable(),
new Ability(Abilities.WATER_ABSORB, 3)
.attr(TypeImmunityHealAbAttr, Type.WATER)
.partial() // Healing not blocked by Heal Block
.ignorable(),
new Ability(Abilities.OBLIVIOUS, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.INFATUATED)
@ -4948,8 +4959,7 @@ export function initAbilities() {
.attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.hasFlag(MoveFlags.SOUND_BASED))
.ignorable(),
new Ability(Abilities.RAIN_DISH, 3)
.attr(PostWeatherLapseHealAbAttr, 1, WeatherType.RAIN, WeatherType.HEAVY_RAIN)
.partial(), // Healing not blocked by Heal Block
.attr(PostWeatherLapseHealAbAttr, 1, WeatherType.RAIN, WeatherType.HEAVY_RAIN),
new Ability(Abilities.SAND_STREAM, 3)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SANDSTORM)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SANDSTORM),
@ -5080,7 +5090,6 @@ export function initAbilities() {
.attr(PostWeatherLapseHealAbAttr, 2, WeatherType.RAIN, WeatherType.HEAVY_RAIN)
.attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 1.25)
.attr(TypeImmunityHealAbAttr, Type.WATER)
.partial() // Healing not blocked by Heal Block
.ignorable(),
new Ability(Abilities.DOWNLOAD, 4)
.attr(DownloadAbAttr),
@ -5161,8 +5170,7 @@ export function initAbilities() {
.ignorable(),
new Ability(Abilities.ICE_BODY, 4)
.attr(BlockWeatherDamageAttr, WeatherType.HAIL)
.attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL, WeatherType.SNOW)
.partial(), // Healing not blocked by Heal Block
.attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL, WeatherType.SNOW),
new Ability(Abilities.SOLID_ROCK, 4)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75)
.ignorable(),
@ -5332,8 +5340,7 @@ export function initAbilities() {
.ignorable()
.unimplemented(),
new Ability(Abilities.CHEEK_POUCH, 6)
.attr(HealFromBerryUseAbAttr, 1/3)
.partial(), // Healing not blocked by Heal Block
.attr(HealFromBerryUseAbAttr, 1/3),
new Ability(Abilities.PROTEAN, 6)
.attr(PokemonTypeChangeAbAttr),
//.condition((p) => !p.summonData?.abilitiesApplied.includes(Abilities.PROTEAN)), //Gen 9 Implementation

View File

@ -3,7 +3,7 @@ import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
import { StatusEffect } from "./status-effect";
import * as Utils from "../utils";
import { ChargeAttr, MoveFlags, allMoves } from "./move";
import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr } from "./move";
import { Type } from "./type";
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability";
import { TerrainType } from "./terrain";
@ -141,6 +141,18 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
*/
abstract isMoveRestricted(move: Moves): boolean;
/**
* Checks if this tag is restricting a move based on a user's decisions during the target selection phase
*
* @param {Moves} move {@linkcode Moves} move ID to check restriction for
* @param {Pokemon} user {@linkcode Pokemon} the user of the above move
* @param {Pokemon} target {@linkcode Pokemon} the target of the above move
* @returns {boolean} `false` unless overridden by the child tag
*/
isMoveTargetRestricted(move: Moves, user: Pokemon, target: Pokemon): boolean {
return false;
}
/**
* Gets the text to display when the player attempts to select a move that is restricted by this tag.
*
@ -2178,6 +2190,74 @@ export class ExposedTag extends BattlerTag {
}
}
/**
* Tag that prevents HP recovery from held items and move effects. It also blocks the usage of recovery moves.
* Applied by moves: {@linkcode Moves.HEAL_BLOCK | Heal Block (5 turns)}, {@linkcode Moves.PSYCHIC_NOISE | Psychic Noise (2 turns)}
*
* @extends MoveRestrictionBattlerTag
*/
export class HealBlockTag extends MoveRestrictionBattlerTag {
constructor(turnCount: number, sourceMove: Moves) {
super(BattlerTagType.HEAL_BLOCK, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], turnCount, sourceMove);
}
onActivation(pokemon: Pokemon): string {
return i18next.t("battle:battlerTagsHealBlock", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) });
}
/**
* Checks if a move is disabled under Heal Block
* @param {Moves} move {@linkcode Moves} the move ID
* @returns `true` if the move has a TRIAGE_MOVE flag and is a status move
*/
override isMoveRestricted(move: Moves): boolean {
if (allMoves[move].hasFlag(MoveFlags.TRIAGE_MOVE) && allMoves[move].category === MoveCategory.STATUS) {
return true;
}
return false;
}
/**
* Checks if a move is disabled under Heal Block because of its choice of target
* Implemented b/c of Pollen Puff
* @param {Moves} move {@linkcode Moves} the move ID
* @param {Pokemon} user {@linkcode Pokemon} the move user
* @param {Pokemon} target {@linkcode Pokemon} the target of the move
* @returns `true` if the move cannot be used because the target is an ally
*/
override isMoveTargetRestricted(move: Moves, user: Pokemon, target: Pokemon) {
const moveCategory = new Utils.IntegerHolder(allMoves[move].category);
applyMoveAttrs(StatusCategoryOnAllyAttr, user, target, allMoves[move], moveCategory);
if (allMoves[move].hasAttr(HealOnAllyAttr) && moveCategory.value === MoveCategory.STATUS ) {
return true;
}
return false;
}
/**
* Uses DisabledTag's selectionDeniedText() message
*/
override selectionDeniedText(pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name });
}
/**
* @override
* @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move
* @param {Moves} move {@linkcode Moves} ID of the move being interrupted
* @returns {string} text to display when the move is interrupted
*/
override interruptedText(pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name });
}
override onRemove(pokemon: Pokemon): void {
super.onRemove(pokemon);
pokemon.scene.queueMessage(i18next.t("battle:battlerTagsHealBlockOnRemove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), null, false, null);
}
}
/**
* Tag that doubles the type effectiveness of Fire-type moves.
* @extends BattlerTag
@ -2490,6 +2570,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new SubstituteTag(sourceMove, sourceId);
case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON:
return new MysteryEncounterPostSummonTag();
case BattlerTagType.HEAL_BLOCK:
return new HealBlockTag(turnCount, sourceMove);
case BattlerTagType.NONE:
default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -867,9 +867,17 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
{
encounter: [
"dialogue:star_grunt.encounter.1",
"dialogue:star_grunt.encounter.2",
"dialogue:star_grunt.encounter.3",
"dialogue:star_grunt.encounter.4",
"dialogue:star_grunt.encounter.5",
],
victory: [
"dialogue:star_grunt.victory.1",
"dialogue:star_grunt.victory.2",
"dialogue:star_grunt.victory.3",
"dialogue:star_grunt.victory.4",
"dialogue:star_grunt.victory.5",
]
}
],
@ -877,9 +885,11 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
{
encounter: [
"dialogue:giacomo.encounter.1",
"dialogue:giacomo.encounter.2",
],
victory: [
"dialogue:giacomo.victory.1",
"dialogue:giacomo.victory.2",
]
}
],
@ -887,9 +897,11 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
{
encounter: [
"dialogue:mela.encounter.1",
"dialogue:mela.encounter.2",
],
victory: [
"dialogue:mela.victory.1",
"dialogue:mela.victory.2",
]
}
],
@ -897,9 +909,11 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
{
encounter: [
"dialogue:atticus.encounter.1",
"dialogue:atticus.encounter.2",
],
victory: [
"dialogue:atticus.victory.1",
"dialogue:atticus.victory.2",
]
}
],
@ -907,9 +921,11 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
{
encounter: [
"dialogue:ortega.encounter.1",
"dialogue:ortega.encounter.2",
],
victory: [
"dialogue:ortega.victory.1",
"dialogue:ortega.victory.2",
]
}
],
@ -917,9 +933,11 @@ export const trainerTypeDialogue: TrainerTypeDialogue = {
{
encounter: [
"dialogue:eri.encounter.1",
"dialogue:eri.encounter.2",
],
victory: [
"dialogue:eri.victory.1",
"dialogue:eri.victory.2",
]
}
],

View File

@ -4539,6 +4539,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
case BattlerTagType.NIGHTMARE:
case BattlerTagType.DROWSY:
case BattlerTagType.DISABLED:
case BattlerTagType.HEAL_BLOCK:
return -5;
case BattlerTagType.SEEDED:
case BattlerTagType.SALT_CURED:
@ -7826,8 +7827,8 @@ export function initMoves() {
.makesContact()
.attr(LessPPMorePowerAttr),
new StatusMove(Moves.HEAL_BLOCK, Type.PSYCHIC, 100, 15, -1, 0, 4)
.target(MoveTarget.ALL_NEAR_ENEMIES)
.unimplemented(),
.attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, true, 5)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.WRING_OUT, Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 4)
.attr(OpponentHighHpPowerAttr, 120)
.makesContact(),
@ -9609,7 +9610,7 @@ export function initMoves() {
.recklessMove(),
new AttackMove(Moves.PSYCHIC_NOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 75, 100, 10, -1, 0, 9)
.soundBased()
.partial(),
.attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, false, 2),
new AttackMove(Moves.UPPER_HAND, Type.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9)
.attr(FlinchAttr)
.condition((user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].priority > 0 ) // TODO: is this bang correct?

View File

@ -431,14 +431,13 @@ function getPokemonTradeOptions(scene: BattleScene): Map<number, EnemyPokemon[]>
function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?: number): PokemonSpecies {
let newSpecies: PokemonSpecies | undefined;
while (isNullOrUndefined(newSpecies)) {
let bstCap = 9999;
let bstMin = 0;
if (originalBst) {
bstCap = originalBst + 100;
bstMin = originalBst - 100;
}
while (isNullOrUndefined(newSpecies)) {
// Get all non-legendary species that fall within the Bst range requirements
let validSpecies = allSpecies
.filter(s => {

View File

@ -2261,8 +2261,8 @@ export const trainerConfigs: TrainerConfigs = {
[TrainerType.PENNY_2]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", [], true).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.REVAVROOM ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.formIndex = Utils.randSeedInt(5, 1); //Random Starmobile form
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.ENTEI, Species.RAIKOU, Species.SUICUNE ], TrainerSlot.TRAINER, true, p => {

View File

@ -80,4 +80,5 @@ export enum BattlerTagType {
BURNED_UP = "BURNED_UP",
DOUBLE_SHOCKED = "DOUBLE_SHOCKED",
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON",
HEAL_BLOCK = "HEAL_BLOCK",
}

View File

@ -2971,16 +2971,40 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return this.getRestrictingTag(moveId) !== null;
}
/**
* Gets whether the given move is currently disabled for the user based on the player's target selection
*
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
* @param {Pokemon} user {@linkcode Pokemon} the move user
* @param {Pokemon} target {@linkcode Pokemon} the target of the move
*
* @returns {boolean} `true` if the move is disabled for this Pokemon due to the player's target selection
*
* @see {@linkcode MoveRestrictionBattlerTag}
*/
isMoveTargetRestricted(moveId: Moves, user: Pokemon, target: Pokemon): boolean {
for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) {
if ((tag as MoveRestrictionBattlerTag).isMoveTargetRestricted(moveId, user, target)) {
return (tag as MoveRestrictionBattlerTag !== null);
}
}
return false;
}
/**
* Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists.
*
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
* @param {Pokemon} user {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status
* @param {Pokemon} target {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status
* @returns {MoveRestrictionBattlerTag | null} the first tag on this Pokemon that restricts the move, or `null` if the move is not restricted.
*/
getRestrictingTag(moveId: Moves): MoveRestrictionBattlerTag | null {
getRestrictingTag(moveId: Moves, user?: Pokemon, target?: Pokemon): MoveRestrictionBattlerTag | null {
for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) {
if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId)) {
return tag as MoveRestrictionBattlerTag;
} else if (user && target && (tag as MoveRestrictionBattlerTag).isMoveTargetRestricted(moveId, user, target)) {
return tag as MoveRestrictionBattlerTag;
}
}
return null;

View File

@ -425,14 +425,32 @@ export default class Trainer extends Phaser.GameObjects.Container {
}
}
if (retry && (attempt || 0) < 10) {
// Prompts reroll of party member species if species already present in the enemy party
if (this.checkDuplicateSpecies(ret)) {
console.log("Duplicate species detected, prompting reroll...");
retry = true;
}
if (retry && (attempt ?? 0) < 10) {
console.log("Rerolling party member...");
ret = this.genNewPartyMemberSpecies(level, strength, (attempt || 0) + 1);
ret = this.genNewPartyMemberSpecies(level, strength, (attempt ?? 0) + 1);
}
return ret;
}
/**
* Checks if the enemy trainer already has the Pokemon species in their party
* @param {PokemonSpecies} species {@linkcode PokemonSpecies}
* @returns `true` if the species is already present in the party
*/
checkDuplicateSpecies(species: PokemonSpecies): boolean {
const currentPartySpecies = this.scene.getEnemyParty().map(p => {
return p.species.speciesId;
});
return currentPartySpecies.includes(species.speciesId);
}
getPartyMemberMatchupScores(trainerSlot: TrainerSlot = TrainerSlot.NONE, forSwitch: boolean = false): [integer, integer][] {
if (trainerSlot && !this.isDouble()) {
trainerSlot = TrainerSlot.NONE;

View File

@ -96,5 +96,7 @@
"unlockedSomething": "{{unlockedThing}} wurde freigeschaltet.",
"congratulations": "Glückwunsch!",
"beatModeFirstTime": "{{speciesName}} hat den {{gameMode}} Modus zum ersten Mal beendet! Du erhältst {{newModifier}}!",
"eggSkipPrompt": "Zur Ei-Zusammenfassung springen?"
"eggSkipPrompt": "Zur Ei-Zusammenfassung springen?",
"battlerTagsHealBlock": "{{pokemonNameWithAffix}} kann nicht geheilt werden, da die Heilung blockiert wird!",
"battlerTagsHealBlockOnRemove": "{{pokemonNameWithAffix}} kann wieder geheilt werden!"
}

View File

@ -105,5 +105,7 @@
"congratulations": "Congratulations!",
"beatModeFirstTime": "{{speciesName}} beat {{gameMode}} Mode for the first time!\nYou received {{newModifier}}!",
"ppReduced": "It reduced the PP of {{targetName}}'s\n{{moveName}} by {{reduction}}!",
"mysteryEncounterAppeared": "What's this?"
"mysteryEncounterAppeared": "What's this?",
"battlerTagsHealBlock": "{{pokemonNameWithAffix}} can't restore its HP!",
"battlerTagsHealBlockOnRemove": "{{pokemonNameWithAffix}} can restore its HP again!"
}

View File

@ -85,5 +85,7 @@
"statSeverelyFell_one": "¡El {{stats}} de {{pokemonNameWithAffix}} ha bajado muchísimo!",
"statSeverelyFell_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} han bajado muchísimo!",
"statWontGoAnyLower_one": "¡El {{stats}} de {{pokemonNameWithAffix}} no puede bajar más!",
"statWontGoAnyLower_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} no pueden bajar más!"
"statWontGoAnyLower_other": "¡{{stats}} de\n{{pokemonNameWithAffix}} no pueden bajar más!",
"battlerTagsHealBlock": "¡{{pokemonNameWithAffix}} no puede restaurar sus PS!",
"battlerTagsHealBlockOnRemove": "¡{{pokemonNameWithAffix}} ya puede recuperar PS!"
}

View File

@ -99,5 +99,7 @@
"unlockedSomething": "{{unlockedThing}}\na été débloqué.",
"congratulations": "Félicitations !",
"beatModeFirstTime": "{{speciesName}} a battu le mode {{gameMode}} pour la première fois !\nVous avez reçu {{newModifier}} !",
"eggSkipPrompt": "Aller directement au résumé des Œufs éclos ?"
"eggSkipPrompt": "Aller directement au résumé des Œufs éclos ?",
"battlerTagsHealBlock": "{{pokemonNameWithAffix}} ne peut pas guérir !",
"battlerTagsHealBlockOnRemove": "Le blocage de soins qui affectait\n{{pokemonNameWithAffix}} sest dissipé !"
}

View File

@ -96,5 +96,7 @@
"retryBattle": "Você gostaria de tentar novamente desde o início da batalha?",
"unlockedSomething": "{{unlockedThing}}\nfoi desbloqueado.",
"congratulations": "Parabéns!",
"beatModeFirstTime": "{{speciesName}} venceu o Modo {{gameMode}} pela primeira vez!\nVocê recebeu {{newModifier}}!"
"beatModeFirstTime": "{{speciesName}} venceu o Modo {{gameMode}} pela primeira vez!\nVocê recebeu {{newModifier}}!",
"battlerTagsHealBlock": "{{pokemonNameWithAffix}} não pode restaurar seus PS!",
"battlerTagsHealBlockOnRemove": "{{pokemonNameWithAffix}} pode restaurar seus PS novamente!"
}

View File

@ -413,7 +413,7 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier
}
/**
* Modifies the chance of a double battle occurring
* Increases the chance of a double battle occurring
* @param args [0] {@linkcode Utils.NumberHolder} for double battle chance
* @returns true if the modifier was applied
*/
@ -421,7 +421,7 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier
const doubleBattleChance = args[0] as Utils.NumberHolder;
// This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using Utils.randSeedInt
// A double battle will initiate if the generated number is 0
doubleBattleChance.value = Math.ceil(doubleBattleChance.value / 4);
doubleBattleChance.value = doubleBattleChance.value / 4;
return true;
}

View File

@ -10,6 +10,8 @@ import { HealAchv } from "#app/system/achv";
import i18next from "i18next";
import * as Utils from "#app/utils";
import { CommonAnimPhase } from "./common-anim-phase";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { HealBlockTag } from "#app/data/battler-tags";
export class PokemonHealPhase extends CommonAnimPhase {
private hpHealed: integer;
@ -50,9 +52,14 @@ export class PokemonHealPhase extends CommonAnimPhase {
const hasMessage = !!this.message;
const healOrDamage = (!pokemon.isFullHp() || this.hpHealed < 0);
const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag;
let lastStatusEffect = StatusEffect.NONE;
if (healOrDamage) {
if (healBlock && this.hpHealed > 0) {
this.scene.queueMessage(healBlock.onActivation(pokemon));
this.message = null;
super.end();
} else if (healOrDamage) {
const hpRestoreMultiplier = new Utils.IntegerHolder(1);
if (!this.revive) {
this.scene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier);

View File

@ -4,6 +4,8 @@ import { Command } from "#app/ui/command-ui-handler";
import { Mode } from "#app/ui/ui";
import { CommandPhase } from "./command-phase";
import { PokemonPhase } from "./pokemon-phase";
import i18next from "#app/plugins/i18n";
import { allMoves } from "#app/data/move";
export class SelectTargetPhase extends PokemonPhase {
constructor(scene: BattleScene, fieldIndex: integer) {
@ -17,6 +19,14 @@ export class SelectTargetPhase extends PokemonPhase {
const move = turnCommand?.move?.move;
this.scene.ui.setMode(Mode.TARGET_SELECT, this.fieldIndex, move, (targets: BattlerIndex[]) => {
this.scene.ui.setMode(Mode.MESSAGE);
const fieldSide = this.scene.getField();
const user = fieldSide[this.fieldIndex];
const moveObject = allMoves[move!];
if (moveObject && user.isMoveTargetRestricted(moveObject.id, user, fieldSide[targets[0]])) {
const errorMessage = user.getRestrictingTag(move!, user, fieldSide[targets[0]])!.selectionDeniedText(user, moveObject.id);
user.scene.queueMessage(i18next.t(errorMessage, { moveName: moveObject.name }), 0, true);
targets = [];
}
if (targets.length < 1) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = null;
this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex));

View File

@ -0,0 +1,59 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
describe("Abilities - Arena Trap", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.SPLASH)
.ability(Abilities.ARENA_TRAP)
.enemySpecies(Species.RALTS)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TELEPORT);
});
// TODO: Enable test when Issue #935 is addressed
it.todo("should not allow grounded Pokémon to flee", async () => {
game.override.battleType("single");
await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon();
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(enemy).toBe(game.scene.getEnemyPokemon());
}, TIMEOUT);
it("should guarantee double battle with any one LURE", async () => {
game.override
.startingModifier([
{ name: "LURE" },
])
.startingWave(2);
await game.classicMode.startBattle();
expect(game.scene.getEnemyField().length).toBe(2);
}, TIMEOUT);
});

View File

@ -0,0 +1,59 @@
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
describe("Abilities - Illuminate", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.SPLASH)
.ability(Abilities.ILLUMINATE)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SAND_ATTACK);
});
it("should prevent ACC stat stage from being lowered", async () => {
game.override.battleType("single");
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
expect(player.getStatStage(Stat.ACC)).toBe(0);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.getStatStage(Stat.ACC)).toBe(0);
}, TIMEOUT);
it("should guarantee double battle with any one LURE", async () => {
game.override
.startingModifier([
{ name: "LURE" },
])
.startingWave(2);
await game.classicMode.startBattle();
expect(game.scene.getEnemyField().length).toBe(2);
}, TIMEOUT);
});

View File

@ -0,0 +1,68 @@
import { BattlerIndex } from "#app/battle";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
describe("Abilities - No Guard", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.ZAP_CANNON)
.ability(Abilities.NO_GUARD)
.enemyLevel(200)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should make moves always hit regardless of move accuracy", async () => {
game.override.battleType("single");
await game.classicMode.startBattle([
Species.REGIELEKI
]);
game.move.select(Moves.ZAP_CANNON);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
const moveEffectPhase = game.scene.getCurrentPhase() as MoveEffectPhase;
vi.spyOn(moveEffectPhase, "hitCheck");
await game.phaseInterceptor.to(MoveEndPhase);
expect(moveEffectPhase.hitCheck).toHaveReturnedWith(true);
}, TIMEOUT);
it("should guarantee double battle with any one LURE", async () => {
game.override
.startingModifier([
{ name: "LURE" },
])
.startingWave(2);
await game.classicMode.startBattle();
expect(game.scene.getEnemyField().length).toBe(2);
}, TIMEOUT);
});

View File

@ -0,0 +1,153 @@
import { BattlerIndex } from "#app/battle";
import { ArenaTagSide } from "#app/data/arena-tag";
import { WeatherType } from "#app/data/weather";
import GameManager from "#app/test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
const TIMEOUT = 20 * 1000;
// Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Heal_Block_(move)
describe("Moves - Heal Block", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.ABSORB, Moves.WISH, Moves.SPLASH, Moves.AQUA_RING])
.enemyMoveset(Moves.HEAL_BLOCK)
.ability(Abilities.NO_GUARD)
.enemyAbility(Abilities.BALL_FETCH)
.enemySpecies(Species.BLISSEY)
.disableCrits();
});
it("shouldn't stop damage from HP-drain attacks, just HP restoration", async() => {
await game.classicMode.startBattle([Species.CHARIZARD]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
player.damageAndUpdate(enemy.getMaxHp() - 1);
game.move.select(Moves.ABSORB);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBe(1);
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
}, TIMEOUT
);
it("shouldn't stop Liquid Ooze from dealing damage", async() => {
game.override.enemyAbility(Abilities.LIQUID_OOZE);
await game.classicMode.startBattle([Species.CHARIZARD]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.ABSORB);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(player.isFullHp()).toBe(false);
expect(enemy.isFullHp()).toBe(false);
}, TIMEOUT);
it("should stop delayed heals, such as from Wish", async() => {
await game.classicMode.startBattle([Species.CHARIZARD]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.getMaxHp() - 1);
game.move.select(Moves.WISH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.WISH, ArenaTagSide.PLAYER)).toBeDefined();
while (game.scene.arena.getTagOnSide(ArenaTagType.WISH, ArenaTagSide.PLAYER)) {
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
}
expect(player.hp).toBe(1);
}, TIMEOUT);
it("should prevent Grassy Terrain from restoring HP", async() => {
game.override.enemyAbility(Abilities.GRASSY_SURGE);
await game.classicMode.startBattle([Species.CHARIZARD]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.getMaxHp() - 1);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBe(1);
}, TIMEOUT);
it("should prevent healing from heal-over-time moves", async() => {
await game.classicMode.startBattle([Species.CHARIZARD]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.getMaxHp() - 1);
game.move.select(Moves.AQUA_RING);
await game.phaseInterceptor.to("TurnEndPhase");
expect(player.getTag(BattlerTagType.AQUA_RING)).toBeDefined();
expect(player.hp).toBe(1);
}, TIMEOUT);
it("should prevent abilities from restoring HP", async() => {
game.override
.weather(WeatherType.RAIN)
.ability(Abilities.RAIN_DISH);
await game.classicMode.startBattle([Species.CHARIZARD]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.getMaxHp() - 1);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBe(1);
}, TIMEOUT);
it("should stop healing from items", async() => {
game.override.startingHeldItems([{name: "LEFTOVERS"}]);
await game.classicMode.startBattle([Species.CHARIZARD]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.getMaxHp() - 1);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBe(1);
}, TIMEOUT);
});

View File

@ -69,6 +69,20 @@ describe("Global Trade System - Mystery Encounter", () => {
expect(GlobalTradeSystemEncounter.options.length).toBe(4);
});
it("should not loop infinitely when generating trade options for extreme BST non-legendaries", async () => {
const extremeBstTeam = [Species.SLAKING, Species.WISHIWASHI, Species.SUNKERN];
await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, extremeBstTeam);
expect(GlobalTradeSystemEncounter.encounterType).toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM);
expect(GlobalTradeSystemEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON);
expect(GlobalTradeSystemEncounter.dialogue).toBeDefined();
expect(GlobalTradeSystemEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]);
expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`);
expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`);
expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`);
expect(GlobalTradeSystemEncounter.options.length).toBe(4);
});
it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingBiome(Biome.VOLCANO);