Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Move] Implement Heal Block #4120

Merged
merged 17 commits into from
Sep 21, 2024
Merged
12 changes: 3 additions & 9 deletions src/data/ability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4831,11 +4831,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)
Expand Down Expand Up @@ -4948,8 +4946,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),
Expand Down Expand Up @@ -5080,7 +5077,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),
Expand Down Expand Up @@ -5161,8 +5157,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(),
Expand Down Expand Up @@ -5332,8 +5327,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
Expand Down
91 changes: 90 additions & 1 deletion src/data/battler-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -2178,6 +2190,81 @@ 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);
}

/**
* Uses the default onAdd method
*/
override onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon);
}

frutescens marked this conversation as resolved.
Show resolved Hide resolved
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 {boolean} T/F if the move has a TRIAGE_MOVE flag and is a status move
frutescens marked this conversation as resolved.
Show resolved Hide resolved
*/
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 {boolean} the move cannot be used b/c the target is an ally
frutescens marked this conversation as resolved.
Show resolved Hide resolved
*/
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
Expand Down Expand Up @@ -2490,6 +2577,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);
Expand Down
11 changes: 7 additions & 4 deletions src/data/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3969,8 +3969,10 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr {
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.IntegerHolder);
// Need to check for strict equality or something better than this.
const isAlly = (user.isPlayer() && target.isPlayer()) || (!user.isPlayer() && !target.isPlayer());
frutescens marked this conversation as resolved.
Show resolved Hide resolved
frutescens marked this conversation as resolved.
Show resolved Hide resolved

if (user.getAlly() === target) {
if (isAlly) {
category.value = MoveCategory.STATUS;
return true;
}
Expand Down Expand Up @@ -4539,6 +4541,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:
Expand Down Expand Up @@ -7826,8 +7829,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(),
Expand Down Expand Up @@ -9609,7 +9612,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?
Expand Down
1 change: 1 addition & 0 deletions src/enums/battler-tag-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
26 changes: 25 additions & 1 deletion src/field/pokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/locales/de/battle.json
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
}
4 changes: 3 additions & 1 deletion src/locales/en/battle.json
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
}
4 changes: 3 additions & 1 deletion src/locales/es/battle.json
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
}
4 changes: 3 additions & 1 deletion src/locales/fr/battle.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}} s’est dissipé !"
}
4 changes: 3 additions & 1 deletion src/locales/pt_BR/battle.json
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
}
9 changes: 8 additions & 1 deletion src/phases/pokemon-heal-phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions src/phases/select-target-phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.length = 0;
frutescens marked this conversation as resolved.
Show resolved Hide resolved
}
if (targets.length < 1) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = null;
this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex));
Expand Down
Loading
Loading