diff --git a/src/data/move.ts b/src/data/move.ts index a1379241a6c..a650d75b150 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -755,7 +755,7 @@ export default class Move implements Localizable { * @param target {@linkcode Pokemon} The Pokémon being targeted by the move. * @returns The calculated power of the move. */ - calculateBattlePower(source: Pokemon, target: Pokemon, simulated: boolean = false): number { + calculateBattlePower(source: Pokemon, target: Pokemon, simulated: boolean = false, isMin: boolean = false, isMax: boolean = false): number { if (this.category === MoveCategory.STATUS) { return -1; } @@ -770,7 +770,7 @@ export default class Move implements Localizable { power.value = 60; } - applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, target, this, simulated, power); + applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, target, this, simulated, power, isMin, isMax); if (source.getAlly()) { applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, source.getAlly(), target, this, simulated, power); @@ -800,7 +800,7 @@ export default class Move implements Localizable { power.value /= 2; } - applyMoveAttrs(VariablePowerAttr, source, target, this, power); + applyMoveAttrs(VariablePowerAttr, source, target, this, power, isMin, isMax); source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power); @@ -1162,12 +1162,12 @@ export class FixedDamageAttr extends MoveAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - (args[0] as Utils.IntegerHolder).value = this.getDamage(user, target, move); + (args[0] as Utils.IntegerHolder).value = this.getDamage(user, target, move, args); return true; } - getDamage(user: Pokemon, target: Pokemon, move: Move): integer { + getDamage(user: Pokemon, target: Pokemon, move: Move, args: any[]): integer { return this.damage; } } @@ -1251,7 +1251,7 @@ export class LevelDamageAttr extends FixedDamageAttr { super(0); } - getDamage(user: Pokemon, target: Pokemon, move: Move): number { + getDamage(user: Pokemon, target: Pokemon, move: Move, args?: any[]): number { return user.level; } } @@ -1261,11 +1261,11 @@ export class RandomLevelDamageAttr extends FixedDamageAttr { super(0); } - getDamage(user: Pokemon, target: Pokemon, move: Move, isLow?: boolean, isHigh?: boolean): number { - if (isLow) { + getDamage(user: Pokemon, target: Pokemon, move: Move, args: any[]): number { + if (args[1] == "LOW") { return Utils.toDmgValue(user.level * 0.5); } - if (isHigh) { + if (args[1] == "HIGH") { return Utils.toDmgValue(user.level * 1.5); } return Utils.toDmgValue(user.level * (user.randSeedIntRange(50, 150, "Random Damage") * 0.01)); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 9b1d15f9a9c..c0ed3dedeb8 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2547,7 +2547,13 @@ export class ContactHeldItemTransferChanceModifier extends HeldItemTransferModif return super.getArgs().concat(this.chance * 100); } - getTransferredItemCount(): integer { + getTransferredItemCount(simulated?: "PASS" | "FAIL"): integer { + if (simulated == "PASS") { + return 1; + } + if (simulated == "FAIL") { + return 0; + } return Phaser.Math.RND.realInRange(0, 1) < (this.chance * this.getStackCount()) ? 1 : 0; } @@ -2784,6 +2790,9 @@ export class EnemyStatusEffectHealChanceModifier extends EnemyPersistentModifier apply(args: any[]): boolean { const target = (args[0] as Pokemon); + if (args[1]) { + return false; // No RNG rolls + } if (target.status && Phaser.Math.RND.realInRange(0, 1) < (this.chance * this.getStackCount())) { target.scene.queueMessage(getStatusEffectHealText(target.status.effect, getPokemonNameWithAffix(target))); target.resetStatus(); @@ -2824,6 +2833,10 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { apply(args: any[]): boolean { const target = (args[0] as Pokemon); + if (args[1]) { + return false; + } + if (target.battleData.endured || Phaser.Math.RND.realInRange(0, 1) >= (this.chance * this.getStackCount())) { return false; } diff --git a/src/old-phases.ts b/src/old-phases.ts index a3f91202a08..b359fd1e6cb 100644 --- a/src/old-phases.ts +++ b/src/old-phases.ts @@ -4492,7 +4492,7 @@ export class MoveEffectPhase extends PokemonPhase { * made visible to the user until the resulting {@linkcode DamagePhase} * is invoked. */ - const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT; + const hitResult = !isProtected ? target.apply(user, move).hitResult : HitResult.NO_EFFECT; /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ const dealsDamage = [ diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index d6d5ecae87f..846c4bc8d1d 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -181,7 +181,7 @@ export class MoveEffectPhase extends PokemonPhase { * made visible to the user until the resulting {@linkcode DamagePhase} * is invoked. */ - const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT; + const hitResult = !isProtected ? target.apply(user, move) as HitResult : HitResult.NO_EFFECT; /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ const dealsDamage = [ diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 83c945add1d..f0bebd0b8ff 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -18,12 +18,44 @@ import { SwitchSummonPhase } from "./switch-summon-phase"; import { TurnEndPhase } from "./turn-end-phase"; import { WeatherEffectPhase } from "./weather-effect-phase"; import * as LoggerTools from "../logger"; +import { TurnCommand } from "#app/battle.js"; export class TurnStartPhase extends FieldPhase { constructor(scene: BattleScene) { super(scene); } + logTargets(pokemon: Pokemon, mv: PokemonMove, turnCommand: TurnCommand) { + var targets = turnCommand.targets || turnCommand.move!.targets + if (pokemon.isPlayer()) { + console.log(turnCommand.targets, turnCommand.move!.targets) + if (turnCommand.args && turnCommand.args[1] && turnCommand.args[1].isContinuing != undefined) { + console.log(mv.getName(), targets) + } else { + LoggerTools.Actions[pokemon.getBattlerIndex()] = mv.getName() + if (this.scene.currentBattle.double) { + var targIDs = ["Self", "Self", "Ally", "L", "R"] + if (pokemon.getBattlerIndex() == 1) targIDs = ["Self", "Ally", "Self", "L", "R"] + LoggerTools.Actions[pokemon.getBattlerIndex()] += " → " + targets.map(v => targIDs[v+1]) + } else { + var targIDs = ["Self", "", "", "", ""] + var myField = this.scene.getField() + if (myField[0]) + targIDs[1] = myField[0].name + if (myField[1]) + targIDs[2] = myField[1].name + var eField = this.scene.getEnemyField() + if (eField[0]) + targIDs[3] = eField[0].name + if (eField[1]) + targIDs[4] = eField[1].name + //LoggerTools.Actions[pokemon.getBattlerIndex()] += " → " + targets.map(v => targIDs[v+1]) + } + console.log(mv.getName(), targets) + } + } + } + start() { super.start(); @@ -113,8 +145,10 @@ export class TurnStartPhase extends FieldPhase { if (pokemon.isPlayer()) { if (turnCommand.cursor === -1) { this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move));//TODO: is the bang correct here? + this.logTargets(pokemon, move, turnCommand) } else { const playerPhase = new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move, false, queuedMove.ignorePP);//TODO: is the bang correct here? + this.logTargets(pokemon, move, turnCommand) this.scene.pushPhase(playerPhase); } } else { @@ -126,6 +160,10 @@ export class TurnStartPhase extends FieldPhase { break; case Command.POKEMON: this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor!, true, turnCommand.args![0] as boolean, pokemon.isPlayer()));//TODO: is the bang correct here? + if (pokemon.isPlayer()) { + // " " + LoggerTools.playerPokeName(this.scene, pokemon) + + LoggerTools.Actions[pokemon.getBattlerIndex()] = ((turnCommand.args![0] as boolean) ? "Baton" : "Switch") + " to " + LoggerTools.playerPokeName(this.scene, turnCommand.cursor!) + } break; case Command.RUN: let runningPokemon = pokemon; @@ -163,7 +201,7 @@ export class TurnStartPhase extends FieldPhase { this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene)); - this.scene.arenaFlyout.updateFieldText() + this.scene.arenaFlyout.updateFieldText(); if (LoggerTools.Actions.length > 1 && !this.scene.currentBattle.double) { LoggerTools.Actions.pop() // If this is a single battle, but we somehow have two actions, delete the second diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index 69b47e1eaae..0678fba5fe5 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -9,6 +9,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { HitResult } from "#app/field/pokemon.js"; describe("Abilities - Sheer Force", () => { @@ -165,7 +166,7 @@ describe("Abilities - Sheer Force", () => { applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, false, power); - applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, target.apply(user, move)); + applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, (target.apply(user, move) as HitResult)); expect(chance.value).toBe(0); expect(power.value).toBe(move.power * 5461 / 4096); diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 6f2e9760893..05636faeedf 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -5,27 +5,15 @@ import { Command } from "./command-ui-handler"; import { Mode } from "./ui"; import UiHandler from "./ui-handler"; import * as Utils from "../utils"; -import Move, * as MoveData from "../data/move"; +import * as MoveData from "../data/move"; import i18next from "i18next"; import {Button} from "#enums/buttons"; -import { Stat } from "#app/data/pokemon-stat.js"; -import { WeatherType } from "#app/data/weather.js"; -import { Moves } from "#app/enums/moves.js"; -import { AddSecondStrikeAbAttr, AllyMoveCategoryPowerBoostAbAttr, AlwaysHitAbAttr, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreDefendAbAttrsNoApply, BattleStatMultiplierAbAttr, BlockCritAbAttr, BypassBurnDamageReductionAbAttr, ConditionalCritAbAttr, DamageBoostAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentEvasionAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MultCritAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, TypeImmunityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "#app/data/ability.js"; -import { ArenaTagType } from "#app/enums/arena-tag-type.js"; -import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "#app/data/arena-tag.js"; -import { HelpingHandTag, SemiInvulnerableTag, TypeBoostTag } from "#app/data/battler-tags.js"; -import { TerrainType } from "#app/data/terrain.js"; -import { AttackTypeBoosterModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, TempBattleStatBoosterModifier } from "#app/modifier/modifier.js"; -import { BattlerTagType } from "#app/enums/battler-tag-type.js"; -import { TempBattleStat } from "#app/data/temp-battle-stat.js"; -import { StatusEffect } from "#app/data/status-effect.js"; -import { BattleStat } from "#app/data/battle-stat.js"; -import { PokemonMultiHitModifierType } from "#app/modifier/modifier-type.js"; -import { MoveCategory } from "#app/data/move.js"; -import Pokemon, { EnemyPokemon, HitResult, PlayerPokemon, PokemonMove } from "#app/field/pokemon.js"; +import Pokemon, { AttackData, EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon.js"; import { CommandPhase } from "#app/phases/command-phase.js"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase.js"; +import { PokemonMultiHitModifierType } from "#app/modifier/modifier-type.js"; +import { Stat, PermanentStat, PERMANENT_STATS } from "#app/enums/stat.js"; +import { BaseStatModifier } from "#app/modifier/modifier.js"; +import { StatusEffect } from "#app/enums/status-effect.js"; export default class FightUiHandler extends UiHandler { public static readonly MOVES_CONTAINER_NAME = "moves"; @@ -171,552 +159,6 @@ export default class FightUiHandler extends UiHandler { return !this.fieldIndex ? this.cursor : this.cursor2; } - simulateAttack(scene: BattleScene, user: Pokemon, target: Pokemon, move: Move) { - let result: HitResult | undefined = undefined; - const damage1 = new Utils.NumberHolder(0); - const damage2 = new Utils.NumberHolder(0); - const defendingSidePlayField = target.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField(); - - const variableCategory = new Utils.IntegerHolder(move.category); - MoveData.applyMoveAttrs(MoveData.VariableMoveCategoryAttr, user, target, move, variableCategory); - const moveCategory = variableCategory.value as MoveData.MoveCategory; - - const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1); - MoveData.applyMoveAttrs(MoveData.VariableMoveTypeAttr, user, target, move); - const types = target.getTypes(true, true); - - const cancelled = new Utils.BooleanHolder(false); - const typeless = move.hasAttr(MoveData.TypelessAttr); - const typeMultiplier = new Utils.NumberHolder(!typeless && (moveCategory !== MoveData.MoveCategory.STATUS) - ? target.getAttackTypeEffectiveness(move.type, user, false, false) - : 1); - MoveData.applyMoveAttrs(MoveData.VariableMoveTypeMultiplierAttr, user, target, move, typeMultiplier); - if (typeless) { - typeMultiplier.value = 1; - } - if (types.find(t => move.isTypeImmune(user, target, t))) { - typeMultiplier.value = 0; - } - - // Apply arena tags for conditional protection - if (!move.checkFlag(MoveData.MoveFlags.IGNORE_PROTECT, user, target) && !move.isAllyTarget()) { - const defendingSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority); - this.scene.arena.applyTagsForSide(ArenaTagType.WIDE_GUARD, defendingSide, cancelled, this, move.moveTarget); - this.scene.arena.applyTagsForSide(ArenaTagType.MAT_BLOCK, defendingSide, cancelled, this, move.category); - this.scene.arena.applyTagsForSide(ArenaTagType.CRAFTY_SHIELD, defendingSide, cancelled, this, move.category, move.moveTarget); - } - - switch (moveCategory) { - case MoveData.MoveCategory.PHYSICAL: - case MoveData.MoveCategory.SPECIAL: - const isPhysical = moveCategory === MoveData.MoveCategory.PHYSICAL; - const power = new Utils.NumberHolder(move.power); - const sourceTeraType = user.getTeraType(); - if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === move.type && power.value < 60 && move.priority <= 0 && !move.hasAttr(MoveData.MultiHitAttr) && !this.scene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === user.id)) { - power.value = 60; - } - applyPreAttackAbAttrs(VariableMovePowerAbAttr, user, target, move, true, power); - - if (user.getAlly()?.hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) { - applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, user, target, move, true, power); - } - - const fieldAuras = new Set( - this.scene.getField(true) - .map((p) => p.getAbilityAttrs(FieldMoveTypePowerBoostAbAttr) as FieldMoveTypePowerBoostAbAttr[]) - .flat(), - ); - for (const aura of fieldAuras) { - // The only relevant values are `move` and the `power` holder - aura.applyPreAttack(null, null, true, null, move, [power]); - } - - const alliedField: Pokemon[] = user instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField(); - alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, user, move, true, power)); - - power.value *= typeChangeMovePowerMultiplier.value; - - if (!typeless) { - if (target.hasAbilityWithAttr(TypeImmunityAbAttr)) { - // - } - applyPreDefendAbAttrsNoApply(TypeImmunityAbAttr, user, target, move, cancelled, typeMultiplier); - MoveData.applyMoveAttrs(MoveData.NeutralDamageAgainstFlyingTypeMultiplierAttr, user, target, move, typeMultiplier); - } - if (!cancelled.value) { - applyPreDefendAbAttrs(MoveImmunityAbAttr, user, target, move, cancelled, true, typeMultiplier); - defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, user, move, cancelled, true, typeMultiplier)); - } - - if (cancelled.value) { - //user.stopMultiHit(target); - result = HitResult.NO_EFFECT; - } else { - const typeBoost = user.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag; - if (typeBoost) { - power.value *= typeBoost.boostValue; - if (typeBoost.oneUse) { - //user.removeTag(typeBoost.tagType); - } - } - const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(move.type, user.isGrounded())); - MoveData.applyMoveAttrs(MoveData.IgnoreWeatherTypeDebuffAttr, user, target, move, arenaAttackTypeMultiplier); - if (this.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() && move.type === Type.GROUND && move.moveTarget === MoveData.MoveTarget.ALL_NEAR_OTHERS) { - power.value /= 2; - } - - MoveData.applyMoveAttrs(MoveData.VariablePowerAttr, user, target, move, power); - - this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, new Utils.IntegerHolder(0), power); - if (!typeless) { - this.scene.arena.applyTags(WeakenMoveTypeTag, move.type, power); - this.scene.applyModifiers(AttackTypeBoosterModifier, user.isPlayer(), user, move.type, power); - } - if (user.getTag(HelpingHandTag)) { - power.value *= 1.5; - } - let isCritical: boolean = true; - const critOnly = new Utils.BooleanHolder(false); - const critAlways = user.getTag(BattlerTagType.ALWAYS_CRIT); - MoveData.applyMoveAttrs(MoveData.CritOnlyAttr, user, target, move, critOnly); - applyAbAttrs(ConditionalCritAbAttr, user, null, true, critOnly, target, move); - if (isCritical) { - const blockCrit = new Utils.BooleanHolder(false); - applyAbAttrs(BlockCritAbAttr, target, null, true, blockCrit); - if (blockCrit.value) { - isCritical = false; - } - } - const sourceAtk = new Utils.IntegerHolder(user.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, target, move, false)); - const targetDef = new Utils.IntegerHolder(target.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, user, move, false)); - const sourceAtkCrit = new Utils.IntegerHolder(user.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, target, move, isCritical)); - const targetDefCrit = new Utils.IntegerHolder(target.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, user, move, isCritical)); - const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1); - applyAbAttrs(MultCritAbAttr, user, null, true, criticalMultiplier); - const screenMultiplier = new Utils.NumberHolder(1); - if (!isCritical) { - this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, move.category, this.scene.currentBattle.double, screenMultiplier); - } - const isTypeImmune = (typeMultiplier.value * arenaAttackTypeMultiplier.value) === 0; - const sourceTypes = user.getTypes(); - const matchesSourceType = sourceTypes[0] === move.type || (sourceTypes.length > 1 && sourceTypes[1] === move.type); - const stabMultiplier = new Utils.NumberHolder(1); - if (sourceTeraType === Type.UNKNOWN && matchesSourceType) { - stabMultiplier.value += 0.5; - } else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === move.type) { - stabMultiplier.value += 0.5; - } - - applyAbAttrs(StabBoostAbAttr, user, null, true, stabMultiplier); - - if (sourceTeraType !== Type.UNKNOWN && matchesSourceType) { - stabMultiplier.value = Math.min(stabMultiplier.value + 0.5, 2.25); - } - - MoveData.applyMoveAttrs(MoveData.VariableAtkAttr, user, target, move, sourceAtk); - MoveData.applyMoveAttrs(MoveData.VariableDefAttr, user, target, move, targetDef); - MoveData.applyMoveAttrs(MoveData.VariableAtkAttr, user, target, move, sourceAtkCrit); - MoveData.applyMoveAttrs(MoveData.VariableDefAttr, user, target, move, targetDefCrit); - - const effectPhase = this.scene.getCurrentPhase(); - let numTargets = 1; - if (effectPhase instanceof MoveEffectPhase) { - numTargets = effectPhase.getTargets().length; - } - const twoStrikeMultiplier = new Utils.NumberHolder(1); - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, target, move, true, numTargets, new Utils.IntegerHolder(0), twoStrikeMultiplier); - - if (!isTypeImmune) { - damage1.value = Math.ceil(((((2 * user.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * twoStrikeMultiplier.value * 0.85); // low roll - damage2.value = Math.ceil(((((2 * user.level / 5 + 2) * power.value * sourceAtkCrit.value / targetDefCrit.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * twoStrikeMultiplier.value * criticalMultiplier.value); // high roll crit - if (isPhysical && user.status && user.status.effect === StatusEffect.BURN) { - if (!move.hasAttr(MoveData.BypassBurnDamageReductionAttr)) { - const burnDamageReductionCancelled = new Utils.BooleanHolder(false); - applyAbAttrs(BypassBurnDamageReductionAbAttr, user, burnDamageReductionCancelled); - if (!burnDamageReductionCancelled.value) { - damage1.value = Math.floor(damage1.value / 2); - damage2.value = Math.floor(damage2.value / 2); - } - } - } - - applyPreAttackAbAttrs(DamageBoostAbAttr, user, target, move, true, damage1); - applyPreAttackAbAttrs(DamageBoostAbAttr, user, target, move, true, damage2); - - /** - * For each {@link HitsTagAttr} the move has, doubles the damage of the move if: - * The target has a {@link BattlerTagType} that this move interacts with - * AND - * The move doubles damage when used against that tag - */ - move.getAttrs(MoveData.HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { - if (target.getTag(hta.tagType)) { - damage1.value *= 2; - damage2.value *= 2; - } - }); - } - - if (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && target.isGrounded() && move.type === Type.DRAGON) { - damage1.value = Math.floor(damage1.value / 2); - damage2.value = Math.floor(damage2.value / 2); - } - - const fixedDamage1 = new Utils.IntegerHolder(0); - const fixedDamage2 = new Utils.IntegerHolder(0); - MoveData.applyMoveAttrs(MoveData.FixedDamageAttr, user, target, move, fixedDamage1, true, false); - MoveData.applyMoveAttrs(MoveData.FixedDamageAttr, user, target, move, fixedDamage2, false, true); - if (!isTypeImmune && fixedDamage1.value) { - damage1.value = fixedDamage1.value; - isCritical = false; - result = HitResult.EFFECTIVE; - } - if (!isTypeImmune && fixedDamage2.value) { - damage2.value = fixedDamage2.value; - isCritical = false; - result = HitResult.EFFECTIVE; - } - - if (!result) { - if (!typeMultiplier.value) { - result = move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT; - } else { - const oneHitKo = new Utils.BooleanHolder(false); - MoveData.applyMoveAttrs(MoveData.OneHitKOAttr, user, target, move, oneHitKo); - if (oneHitKo.value) { - result = HitResult.ONE_HIT_KO; - isCritical = false; - damage1.value = target.hp; - damage2.value = target.hp; - } else if (typeMultiplier.value >= 2) { - result = HitResult.SUPER_EFFECTIVE; - } else if (typeMultiplier.value >= 1) { - result = HitResult.EFFECTIVE; - } else { - result = HitResult.NOT_VERY_EFFECTIVE; - } - } - } - - if (!fixedDamage1.value) { - if (!user.isPlayer()) { - this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage1); - } else { - this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage1); - } - } - - if (!fixedDamage2.value) { - if (!user.isPlayer()) { - this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage2); - } else { - this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage2); - } - } - - MoveData.applyMoveAttrs(MoveData.ModifiedDamageAttr, user, target, move, damage1); - MoveData.applyMoveAttrs(MoveData.ModifiedDamageAttr, user, target, move, damage2); - applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, user, target, move, cancelled, true, damage1); - applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, user, target, move, cancelled, true, damage2); - - //console.log("damage (min)", damage1.value, move.name, power.value, sourceAtk, targetDef); - //console.log("damage (max)", damage2.value, move.name, power.value, sourceAtkCrit, targetDefCrit); - - // In case of fatal damage, this tag would have gotten cleared before we could lapse it. - const destinyTag = target.getTag(BattlerTagType.DESTINY_BOND); - - const oneHitKo = result === HitResult.ONE_HIT_KO; - if (damage1.value) { - if (target.getHpRatio() === 1) { - applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, target, user, move, cancelled, true, damage1); - } - } - if (damage2.value) { - if (target.getHpRatio() === 1) { - applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, target, user, move, cancelled, true, damage2); - } - } - } - break; - case MoveData.MoveCategory.STATUS: - if (!typeless) { - applyPreDefendAbAttrsNoApply(TypeImmunityAbAttr, target, user, move, cancelled, true, typeMultiplier); - } - if (!cancelled.value) { - applyPreDefendAbAttrs(MoveImmunityAbAttr, target, user, move, cancelled, true, typeMultiplier); - defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, user, move, cancelled, true, typeMultiplier)); - } - if (!typeMultiplier.value) { - return -1 - } - result = cancelled.value || !typeMultiplier.value ? HitResult.NO_EFFECT : HitResult.STATUS; - break; - } - return [damage1.value, damage2.value] - } - - calculateAccuracy(user: Pokemon, target: Pokemon, move: PokemonMove) { - if (this.scene.currentBattle.double && false) { - switch (move.getMove().moveTarget) { - case MoveData.MoveTarget.USER: // Targets yourself - return -1; // Moves targeting yourself always hit - case MoveData.MoveTarget.OTHER: // Targets one Pokemon - return move.getMove().accuracy - case MoveData.MoveTarget.ALL_OTHERS: // Targets all Pokemon - return move.getMove().accuracy; - case MoveData.MoveTarget.NEAR_OTHER: // Targets a Pokemon adjacent to the user - return move.getMove().accuracy; - case MoveData.MoveTarget.ALL_NEAR_OTHERS: // Targets all Pokemon adjacent to the user - return move.getMove().accuracy; - case MoveData.MoveTarget.NEAR_ENEMY: // Targets an opponent adjacent to the user - return move.getMove().accuracy; - case MoveData.MoveTarget.ALL_NEAR_ENEMIES: // Targets all opponents adjacent to the user - return move.getMove().accuracy; - case MoveData.MoveTarget.RANDOM_NEAR_ENEMY: // Targets a random opponent adjacent to the user - return move.getMove().accuracy; - case MoveData.MoveTarget.ALL_ENEMIES: // Targets all opponents - return move.getMove().accuracy; - case MoveData.MoveTarget.ATTACKER: // Counter move - return move.getMove().accuracy; - case MoveData.MoveTarget.NEAR_ALLY: // Targets an adjacent ally - return move.getMove().accuracy; - case MoveData.MoveTarget.ALLY: // Targets an ally - return move.getMove().accuracy; - case MoveData.MoveTarget.USER_OR_NEAR_ALLY: // Targets an ally or yourself - return move.getMove().accuracy; - case MoveData.MoveTarget.USER_AND_ALLIES: // Targets all on your side - return move.getMove().accuracy; - case MoveData.MoveTarget.ALL: // Targets everyone - return move.getMove().accuracy; - case MoveData.MoveTarget.USER_SIDE: // Targets your field - return move.getMove().accuracy; - case MoveData.MoveTarget.ENEMY_SIDE: // Targets enemy field - return -1; // Moves placing entry hazards always hit - case MoveData.MoveTarget.BOTH_SIDES: // Targets the entire field - return move.getMove().accuracy; - case MoveData.MoveTarget.PARTY: // Targets all of the Player's Pokemon, including ones that aren't active - return move.getMove().accuracy; - case MoveData.MoveTarget.CURSE: - return move.getMove().accuracy; - } - } - // Moves targeting the user and entry hazards can't miss - if ([MoveData.MoveTarget.USER, MoveData.MoveTarget.ENEMY_SIDE].includes(move.getMove().moveTarget)) { - return -1; - } - if (target == undefined) return move.getMove().accuracy; - // If either Pokemon has No Guard, - if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { - return -1; - } - // If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match - if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().slice(1).find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { - return -1; - } - - const hiddenTag = target.getTag(SemiInvulnerableTag); - if (hiddenTag && !move.getMove().getAttrs(MoveData.HitsTagAttr).some(hta => hta.tagType === hiddenTag.tagType)) { - return 0; - } - const moveAccuracy = new Utils.NumberHolder(move.getMove().accuracy); - - MoveData.applyMoveAttrs(MoveData.VariableAccuracyAttr, user, target, move.getMove(), moveAccuracy); - applyPreDefendAbAttrs(WonderSkinAbAttr, target, user, move.getMove(), { value: false }, true, moveAccuracy); - - if (moveAccuracy.value === -1) { - return -1; - } - - const isOhko = move.getMove().hasAttr(MoveData.OneHitKOAccuracyAttr); - - if (!isOhko) { - user.scene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); - } - - if (this.scene.arena.weather?.weatherType === WeatherType.FOG) { - moveAccuracy.value = Math.floor(moveAccuracy.value * 0.9); - } - - if (!isOhko && this.scene.arena.getTag(ArenaTagType.GRAVITY)) { - moveAccuracy.value = Math.floor(moveAccuracy.value * 1.67); - } - - const userAccuracyLevel = new Utils.IntegerHolder(user.summonData.battleStats[BattleStat.ACC]); - const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, true, userAccuracyLevel); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, user, null, true, targetEvasionLevel); - applyAbAttrs(IgnoreOpponentEvasionAbAttr, user, null, true, targetEvasionLevel); - MoveData.applyMoveAttrs(MoveData.IgnoreOpponentStatChangesAttr, user, target, move.getMove(), targetEvasionLevel); - this.scene.applyModifiers(TempBattleStatBoosterModifier, user.isPlayer(), TempBattleStat.ACC, userAccuracyLevel); - - const accuracyMultiplier = new Utils.NumberHolder(1); - if (userAccuracyLevel.value !== targetEvasionLevel.value) { - accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value - ? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3 - : 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6)); - } - - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, user, BattleStat.ACC, accuracyMultiplier, true, move.getMove()); - - const evasionMultiplier = new Utils.NumberHolder(1); - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier, true); - - accuracyMultiplier.value /= evasionMultiplier.value; - - return moveAccuracy.value * accuracyMultiplier.value - } - - calcDamage(scene: BattleScene, user: PlayerPokemon, target: Pokemon, move: PokemonMove) { - /* - var power = move.getMove().power - var myAtk = 0 - var theirDef = 0 - var myAtkC = 0 - var theirDefC = 0 - switch (move.getMove().category) { - case MoveData.MoveCategory.PHYSICAL: - myAtk = user.getBattleStat(Stat.ATK, target, move.getMove()) - myAtkC = user.getBattleStat(Stat.ATK, target, move.getMove(), true) - theirDef = target.getBattleStat(Stat.DEF, user, move.getMove()) - theirDefC = target.getBattleStat(Stat.DEF, user, move.getMove(), true) - break; - case MoveData.MoveCategory.SPECIAL: - myAtk = user.getBattleStat(Stat.SPATK, target, move.getMove()) - myAtkC = user.getBattleStat(Stat.SPATK, target, move.getMove(), true) - theirDef = target.getBattleStat(Stat.SPDEF, user, move.getMove()) - theirDefC = target.getBattleStat(Stat.SPDEF, user, move.getMove(), true) - break; - case MoveData.MoveCategory.STATUS: - return "---" - } - var stabBonus = 1 - var types = user.getTypes() - // Apply STAB bonus - for (var i = 0; i < types.length; i++) { - if (types[i] == move.getMove().type) { - stabBonus = 1.5 - } - } - // Apply Tera Type bonus - if (stabBonus == 1.5) { - // STAB - if (move.getMove().type == user.getTeraType()) { - stabBonus = 2 - } - } else if (move.getMove().type == user.getTeraType()) { - stabBonus = 1.5 - } - // Apply adaptability - if (stabBonus == 2) { - // Tera-STAB - if (move.getMove().type == user.getTeraType()) { - stabBonus = 2.25 - } - } else if (stabBonus == 1.5) { - // STAB or Tera - if (move.getMove().type == user.getTeraType()) { - stabBonus = 2 - } - } else if (move.getMove().type == user.getTeraType()) { - // Adaptability - stabBonus = 1.5 - } - var weatherBonus = 1 - if (this.scene.arena.weather.weatherType == WeatherType.RAIN || this.scene.arena.weather.weatherType == WeatherType.HEAVY_RAIN) { - if (move.getMove().type == Type.WATER) { - weatherBonus = 1.5 - } - if (move.getMove().type == Type.FIRE) { - weatherBonus = this.scene.arena.weather.weatherType == WeatherType.HEAVY_RAIN ? 0 : 0.5 - } - } - if (this.scene.arena.weather.weatherType == WeatherType.SUNNY || this.scene.arena.weather.weatherType == WeatherType.HARSH_SUN) { - if (move.getMove().type == Type.FIRE) { - weatherBonus = 1.5 - } - if (move.getMove().type == Type.WATER) { - weatherBonus = this.scene.arena.weather.weatherType == WeatherType.HARSH_SUN ? 0 : (move.moveId == Moves.HYDRO_STEAM ? 1.5 : 0.5) - } - } - var typeBonus = target.getAttackMoveEffectiveness(user, move) - var modifiers = stabBonus * weatherBonus - */ - var dmgHigh = 0 - var dmgLow = 0 - // dmgLow = (((2*user.level/5 + 2) * power * myAtk / theirDef)/50 + 2) * 0.85 * modifiers - // dmgHigh = (((2*user.level/5 + 2) * power * myAtkC / theirDefC)/50 + 2) * 1.5 * modifiers - var out = this.simulateAttack(scene, user, target, move.getMove()) - var minHits = 1 - var maxHits = 1 - var mh = move.getMove().getAttrs(MoveData.MultiHitAttr) - for (var i = 0; i < mh.length; i++) { - var mh2 = mh[i] as MoveData.MultiHitAttr - switch (mh2.multiHitType) { - case MoveData.MultiHitType._2: - minHits = 2; - maxHits = 2; - case MoveData.MultiHitType._2_TO_5: - minHits = 2; - maxHits = 5; - case MoveData.MultiHitType._3: - minHits = 3; - maxHits = 3; - case MoveData.MultiHitType._10: - minHits = 10; - maxHits = 10; - case MoveData.MultiHitType.BEAT_UP: - const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); - // No status means the ally pokemon can contribute to Beat Up - minHits = party.reduce((total, pokemon) => { - return total + (pokemon.id === user.id ? 1 : pokemon?.status && pokemon.status.effect !== StatusEffect.NONE ? 0 : 1); - }, 0); - maxHits = minHits - } - } - var h = user.getHeldItems() - for (var i = 0; i < h.length; i++) { - if (h[i].type instanceof PokemonMultiHitModifierType) { - minHits += h[i].getStackCount() - maxHits += h[i].getStackCount() - } - } - dmgLow = out[0] * minHits - dmgHigh = out[1] * maxHits - var qSuffix = "" - if (target.isBoss()) { - var bossSegs = (target as EnemyPokemon).bossSegments - //dmgLow /= bossSegs - //dmgHigh /= bossSegs - //qSuffix = "?" - } - var dmgLowP = Math.round((dmgLow)/target.getBattleStat(Stat.HP)*100) - var dmgHighP = Math.round((dmgHigh)/target.getBattleStat(Stat.HP)*100) - /* - if (user.hasAbility(Abilities.PARENTAL_BOND)) { - // Second hit deals 0.25x damage - dmgLow *= 1.25 - dmgHigh *= 1.25 - } - */ - var koText = "" - if (Math.floor(dmgLow) >= target.hp) { - koText = " (KO)" - } else if (Math.ceil(dmgHigh) >= target.hp) { - var percentChance = 1 - ((target.hp - dmgLow + 1) / (dmgHigh - dmgLow + 1)) - koText = " (" + Math.round(percentChance * 100) + "% KO)" - } - if (target.getMoveEffectiveness(user, move.getMove(), false, true) == undefined) { - return "---" - } - if (scene.damageDisplay == "Value") - return target.getMoveEffectiveness(user, move.getMove(), false, true) + "x - " + (Math.round(dmgLow) == Math.round(dmgHigh) ? Math.round(dmgLow).toString() + qSuffix : Math.round(dmgLow) + "-" + Math.round(dmgHigh) + qSuffix) + koText - if (scene.damageDisplay == "Percent") - return target.getMoveEffectiveness(user, move.getMove(), false, true) + "x - " + (dmgLowP == dmgHighP ? dmgLowP + "%" + qSuffix : dmgLowP + "%-" + dmgHighP + "%" + qSuffix) + koText - if (scene.damageDisplay == "Off") - return target.getMoveEffectiveness(user, move.getMove(), false, true) + "x" + ((Math.floor(dmgLow) >= target.hp) ? " (KO)" : "") - } - setCursor(cursor: integer): boolean { const ui = this.getUi(); @@ -746,22 +188,17 @@ export default class FightUiHandler extends UiHandler { this.typeIcon.setTexture(textureKey, Type[moveType].toLowerCase()).setScale(0.8); const moveCategory = pokemonMove.getMove().category; - this.moveCategoryIcon.setTexture("categories", MoveCategory[moveCategory].toLowerCase()).setScale(1.0); + this.moveCategoryIcon.setTexture("categories", MoveData.MoveCategory[moveCategory].toLowerCase()).setScale(1.0); const power = pokemonMove.getMove().power; const accuracy = pokemonMove.getMove().accuracy; const maxPP = pokemonMove.getMovePp(); const pp = maxPP - pokemonMove.ppUsed; - const accuracy1 = this.calculateAccuracy(pokemon, this.scene.getEnemyField()[0], pokemonMove) - const accuracy2 = this.calculateAccuracy(pokemon, this.scene.getEnemyField()[1], pokemonMove) const ppLeftStr = Utils.padInt(pp, 2, " "); const ppMaxStr = Utils.padInt(maxPP, 2, " "); this.ppText.setText(`${ppLeftStr}/${ppMaxStr}`); this.powerText.setText(`${power >= 0 ? power : "---"}`); this.accuracyText.setText(`${accuracy >= 0 ? accuracy : "---"}`); - this.accuracyText.setText(`${accuracy1 >= 0 ? Math.round(accuracy1) : "---"}`); - if (this.scene.getEnemyField()[1] != undefined) - this.accuracyText.setText(`${accuracy1 >= 0 ? Math.round(accuracy1) : "---"}/${accuracy2 >= 0 ? Math.round(accuracy2) : "---"}`); const ppPercentLeft = pp / maxPP; @@ -781,7 +218,6 @@ export default class FightUiHandler extends UiHandler { pokemon.getOpponents().forEach((opponent) => { opponent.updateEffectiveness(this.getEffectivenessText(pokemon, opponent, pokemonMove)); - opponent.updateEffectiveness(this.calcDamage(this.scene, pokemon, opponent, pokemonMove)); }); } @@ -885,4 +321,80 @@ export default class FightUiHandler extends UiHandler { } this.cursorObj = null; } + + calcDamage(user: PlayerPokemon, target: Pokemon, move: PokemonMove) { + var dmgHigh = 0; + var dmgLow = 0; + var out = target.apply(user, move.getMove(), true) as AttackData + dmgHigh = out.damageHigh; + dmgLow = out.damageLow; + var minHits = 1 + var maxHits = 1 + var mh = move.getMove().getAttrs(MoveData.MultiHitAttr) + for (var i = 0; i < mh.length; i++) { + var mh2 = mh[i] as MoveData.MultiHitAttr + switch (mh2.multiHitType) { + case MoveData.MultiHitType._2: + minHits = 2; + maxHits = 2; + case MoveData.MultiHitType._2_TO_5: + minHits = 2; + maxHits = 5; + case MoveData.MultiHitType._3: + minHits = 3; + maxHits = 3; + case MoveData.MultiHitType._10: + minHits = 10; + maxHits = 10; + case MoveData.MultiHitType.BEAT_UP: + const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + // No status means the ally pokemon can contribute to Beat Up + minHits = party.reduce((total, pokemon) => { + return total + (pokemon.id === user.id ? 1 : pokemon?.status && pokemon.status.effect !== StatusEffect.NONE ? 0 : 1); + }, 0); + maxHits = minHits + } + } + var h = user.getHeldItems() + for (var i = 0; i < h.length; i++) { + if (h[i].type instanceof PokemonMultiHitModifierType) { + minHits += h[i].getStackCount() + maxHits += h[i].getStackCount() + } + } + dmgLow = out[0] * minHits + dmgHigh = out[1] * maxHits + var qSuffix = "" + if (target.isBoss()) { + var bossSegs = (target as EnemyPokemon).bossSegments + //dmgLow /= bossSegs + //dmgHigh /= bossSegs + //qSuffix = "?" + } + var dmgLowP = Math.round((dmgLow)/target.getMaxHp()) + var dmgHighP = Math.round((dmgHigh)/target.getMaxHp()) + /* + if (user.hasAbility(Abilities.PARENTAL_BOND)) { + // Second hit deals 0.25x damage + dmgLow *= 1.25 + dmgHigh *= 1.25 + } + */ + var koText = "" + if (Math.floor(dmgLow) >= target.hp) { + koText = " (KO)" + } else if (Math.ceil(dmgHigh) >= target.hp) { + var percentChance = Utils.rangemap(target.hp, dmgLow, dmgHigh, 0, 1) + koText = " (" + Math.round(percentChance * 100) + "% KO)" + } + if (target.getMoveEffectiveness(user, move.getMove(), false, true) == undefined) { + return "---" + } + if (this.scene.damageDisplay == "Percent") + return target.getMoveEffectiveness(user, move.getMove(), false, true) + "x - " + (dmgLowP == dmgHighP ? dmgLowP + "%" + qSuffix : dmgLowP + "%-" + dmgHighP + "%" + qSuffix) + koText + if (this.scene.damageDisplay == "Value") + return target.getMoveEffectiveness(user, move.getMove(), false, true) + "x" + ((Math.floor(dmgLow) >= target.hp) ? " (KO)" : "") + if (this.scene.damageDisplay == "Off") + return target.getMoveEffectiveness(user, move.getMove(), false, true) + "x" + ((Math.floor(dmgLow) >= target.hp) ? " (KO)" : "") + } } \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 19faec3678a..49ff352d15f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -40,6 +40,10 @@ export function clampInt(value: integer, min: integer, max: integer): integer { return Math.min(Math.max(value, min), max); } +export function rangemap(value: integer, min: integer, max: integer, outMin: integer, outMax: integer) { + return ((value - min) / (max - min)) * (outMax - outMin) + outMin +} + export function randGauss(stdev: number, mean: number = 0): number { if (!stdev) { return 0;