diff --git a/src/data/move.ts b/src/data/move.ts index 57cd1ec63c6..d92832b9fc2 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5259,7 +5259,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } isBatonPass() { - return this.switchType === SwitchType.BATON_PASS; + return this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.MID_TURN_BATON_PASS; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -5277,7 +5277,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); if (switchOutTarget.hp > 0) { - user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); + user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType == SwitchType.SHED_TAIL ? SwitchType.SHED_TAIL : (this.switchType == SwitchType.BATON_PASS ? SwitchType.MID_TURN_BATON_PASS : SwitchType.MID_TURN_SWITCH), switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); return true; } return false; diff --git a/src/enums/switch-type.ts b/src/enums/switch-type.ts index b25ba6ad119..5e26d88857c 100644 --- a/src/enums/switch-type.ts +++ b/src/enums/switch-type.ts @@ -5,8 +5,25 @@ export enum SwitchType { /** Basic switchout where the Pokemon to switch in is selected */ SWITCH, + /** Basic switchout where the Pokemon to switch in is selected + * + * This type is called outside of CommandPhase, and needs its own separate action log + */ + MID_TURN_SWITCH, /** Transfers stat stages and other effects from the returning Pokemon to the switched in Pokemon */ BATON_PASS, - /** Transfers the returning Pokemon's Substitute to the switched in Pokemon */ - SHED_TAIL + /** Transfers stat stages and other effects from the returning Pokemon to the switched in Pokemon + * + * This type is called outside of CommandPhase, and needs its own separate action log + */ + MID_TURN_BATON_PASS, + /** Transfers the returning Pokemon's Substitute to the switched in Pokemon + * + * This type is called outside of CommandPhase, and needs its own separate action log + */ + SHED_TAIL, + /** Basic switchout, but occurring outside of battle */ + PRE_SWITCH, + /** Basic switchout, but occurring outside of battle */ + PRE_BATON_PASS, } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index dd3948bb8bf..558d855b20f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2169,6 +2169,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger); } + /** + * Validates selecting a move. + * @param moveIndex The move's position in the Pokémon's move pool. + * @param ignorePp If `true`, having 0 PP will not make the move be considered invalid. + * @returns Whether the player can successfully choose this move. + */ trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean { const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] @@ -2406,9 +2412,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`). * @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`). * @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`). + * @param isMin if `true`, returns the minimum damage (random power returns lowest possible, crits fail unless guaranteed) (defaults to `false`, overrides `isMax`). + * @param isMax if `true`, returns the maximum damage (random power returns highest possible, crits guaranteed unless immune) (defaults to `false`). * @returns The move's base damage against this Pokemon when used by the source Pokemon. */ - getBaseDamage(source: Pokemon, move: Move, moveCategory: MoveCategory, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { + getBaseDamage(source: Pokemon, move: Move, moveCategory: MoveCategory, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true, isMin: boolean = false, isMax: boolean = false): number { + if (isMin) isMax = false; // If attempting return both minimum and maximum at once, return minimum const isPhysical = moveCategory === MoveCategory.PHYSICAL; /** A base damage multiplier based on the source's level */ @@ -2458,7 +2467,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * - `result`: {@linkcode HitResult} indicates the attack's type effectiveness. * - `damage`: `number` the attack's final damage output. */ - getAttackDamage(source: Pokemon, move: Move, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): DamageCalculationResult { + getAttackDamage(source: Pokemon, move: Move, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: CritResult, simulated: boolean = true): DamageCalculationResult { const damage = new Utils.NumberHolder(0); const damageMin = new Utils.NumberHolder(0); const damageMax = new Utils.NumberHolder(0); @@ -2496,7 +2505,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return { cancelled: cancelled.value, result: move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT, - damage: 0 + damage: 0, + damageHigh: 0, + damageLow: 0, }; } @@ -2507,7 +2518,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return { cancelled: false, result: HitResult.EFFECTIVE, - damage: fixedDamage.value + damage: fixedDamage.value, + damageHigh: fixedDamage.value, + damageLow: fixedDamage.value, }; } @@ -2518,7 +2531,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return { cancelled: false, result: HitResult.ONE_HIT_KO, - damage: this.hp + damage: this.hp, + damageHigh: this.hp, + damageLow: 0, }; } @@ -2526,7 +2541,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * The attack's base damage, as determined by the source's level, move power * and Attack stat as well as this Pokemon's Defense stat */ - const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated); + const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical.isCrit, simulated); + /** The minimum possible base damage, assuming the move always fails to crit, unless some effect guarantees a Critical Hit */ + const baseDamageMin = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical.alwaysCrit, simulated, true); + /** The maximum possible base damage, assuming the move always crits, unless some effect makes the opponent immune to Critical Hits */ + const baseDamageMax = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical.canCrit, simulated, false, true); /** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */ const { targets, multiple } = getMoveTargets(source, move.id); @@ -2546,7 +2565,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** The damage multiplier when the given move critically hits */ - const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1); + const criticalMultiplier = new Utils.NumberHolder(1.5); applyAbAttrs(MultCritAbAttr, source, null, simulated, criticalMultiplier); /** @@ -2609,47 +2628,71 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ? 0.5 : 1; - damage.value = Utils.toDmgValue( - baseDamage + damage.value = 1 * targetMultiplier * parentalBondMultiplier.value * arenaAttackTypeMultiplier.value * glaiveRushMultiplier.value - * criticalMultiplier.value - * randomMultiplier * stabMultiplier.value * typeMultiplier * burnMultiplier.value * screenMultiplier.value * hitsTagMultiplier.value - * mistyTerrainMultiplier - ); + * mistyTerrainMultiplier; + + damageMin.value = damage.value + damageMax.value = damage.value + + // Base Damage + damage.value *= baseDamage + damageMin.value *= baseDamageMin + damageMax.value *= baseDamageMax + + // Critical Hit + damage.value *= isCritical.isCrit ? criticalMultiplier.value : 1 // Applies crit multiplier if the attack was a Critical Hit + damageMin.value *= isCritical.alwaysCrit ? criticalMultiplier.value : 1 // Applies crit multiplier to minimum damage if the attack is guaranteed to crit + damageMax.value *= isCritical.canCrit ? criticalMultiplier.value : 1 // Applies crit multiplier to minimum damage if it is possible for the move to crit + + // Random Damage + damage.value *= randomMultiplier // Random value between 85% and 100%, or 100% if Simulated + damageMin.value *= 0.85 // Lowest possible roll + damageMax.value *= 1 // Highest possible roll + + damage.value = Utils.toDmgValue(damage.value) + damageMin.value = Utils.toDmgValue(damageMin.value) + damageMax.value = Utils.toDmgValue(damageMax.value) + + var damageMultiplier = new Utils.NumberHolder(1); // General multiplier, applied to all three damage calcs (uses a NumberHolder so we don't apply abilities 3 times) /** Doubles damage if the attacker has Tinted Lens and is using a resisted move */ if (!ignoreSourceAbility) { - applyPreAttackAbAttrs(DamageBoostAbAttr, source, this, move, simulated, damage); + applyPreAttackAbAttrs(DamageBoostAbAttr, source, this, move, simulated, damageMultiplier); } /** Apply the enemy's Damage and Resistance tokens */ if (!source.isPlayer()) { - this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage); + this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damageMultiplier); } if (!this.isPlayer()) { - this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage); + this.scene.applyModifiers(EnemyDamageReducerModifier, false, damageMultiplier); } /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ if (!ignoreAbility) { - applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damage); + applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damageMultiplier); } // This attribute may modify damage arbitrarily, so be careful about changing its order of application. applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage); if (this.isFullHp() && !ignoreAbility) { - applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage); + applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, simulated, damageMultiplier); } + damage.value *= damageMultiplier.value + damageMin.value *= damageMultiplier.value + damageMax.value *= damageMultiplier.value + // debug message for when damage is applied (i.e. not simulated) if (!simulated) { console.log("damage", damage.value, move.name); @@ -2667,10 +2710,54 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return { cancelled: cancelled.value, result: hitResult, - damage: damage.value + damage: damage.value, + damageLow: damageMin.value, + damageHigh: damageMax.value, }; } + /** + * Attempts to set a move as a Critical Hit. + * @param source The attacking Pokémon. + * @param move The move that `source` is using against this Pokémon. + * @param simulated Suppresses changes to game state during the calculation (Defaults to `false`). + * @returns Whether the move was a Critical Hit, whether it could be at all, and whether it was guaranteed to be one. + */ + tryCriticalHit(source: Pokemon, move: Move, simulated: boolean = false): CritResult { + const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + + let isCritical: boolean; + let canCrit = true; + let alwaysCrit = false; + + // Calculates whether the move was a Critical Hit or not + const critOnly = new Utils.BooleanHolder(false); + const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT); + applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly); + applyAbAttrs(ConditionalCritAbAttr, source, null, false, critOnly, this, move); + if (critOnly.value || critAlways) { + isCritical = true; + alwaysCrit = true; + } else { + const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))]; + isCritical = simulated ? false : (critChance === 1 || !this.scene.randBattleSeedInt(critChance)); + } + + // Determines if this Pokémon (the target) is immune to Critical Hits, and if so, set isCritical to false + const noCritTag = this.scene.arena.getTagOnSide(NoCritTag, defendingSide); + const blockCrit = new Utils.BooleanHolder(false); + applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit); + if (noCritTag || blockCrit.value || Overrides.NEVER_CRIT_OVERRIDE) { + isCritical = false; + canCrit = false; + } + return { + isCrit: isCritical, + canCrit: canCrit, + alwaysCrit: alwaysCrit + } + } + /** * Applies the results of a move to this pokemon * @param source The {@linkcode Pokemon} using the move @@ -2689,26 +2776,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS; } else { /** Determines whether the attack critically hits */ - let isCritical: boolean; - const critOnly = new Utils.BooleanHolder(false); - const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT); - applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly); - applyAbAttrs(ConditionalCritAbAttr, source, null, false, critOnly, this, move); - if (critOnly.value || critAlways) { - isCritical = true; - } else { - const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))]; - isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); - } + let critical: CritResult = this.tryCriticalHit(source, move); - const noCritTag = this.scene.arena.getTagOnSide(NoCritTag, defendingSide); - const blockCrit = new Utils.BooleanHolder(false); - applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit); - if (noCritTag || blockCrit.value || Overrides.NEVER_CRIT_OVERRIDE) { - isCritical = false; - } - - const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, isCritical, false); + const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, critical, false); const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag; if (typeBoost?.oneUse) { @@ -2749,7 +2819,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * We explicitly require to ignore the faint phase here, as we want to show the messages * about the critical hit and the super effective/not very effective messages before the faint phase. */ - const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); + const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, critical.isCrit, isOneHitKo, isOneHitKo, true); if (damage > 0) { if (source.isPlayer()) { @@ -2762,7 +2832,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { source.turnData.currDamageDealt = damage; this.turnData.damageTaken += damage; this.battleData.hitCount++; - const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; + const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: critical.isCrit, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; this.turnData.attacksReceived.unshift(attackResult); if (source.isPlayer() && !this.isPlayer()) { this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, new Utils.NumberHolder(damage)); @@ -2770,7 +2840,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - if (isCritical) { + if (critical.isCrit) { this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit")); } @@ -3937,6 +4007,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } +export interface CritResult { + /** Whether the move was a Critical Hit */ + isCrit: boolean; + /** Whether the opponent can receive Critical Hits */ + canCrit: boolean; + /** Whether the target is guaranteed to land a Critical Hit with their move */ + alwaysCrit: boolean +} + export default interface Pokemon { scene: BattleScene } @@ -4590,7 +4669,7 @@ export class EnemyPokemon extends Pokemon { return move.category !== MoveCategory.STATUS && moveTargets.some(p => { const doesNotFail = move.applyConditions(this, p, move) || [Moves.SUCKER_PUNCH, Moves.UPPER_HAND, Moves.THUNDERCLAP].includes(move.id); - return doesNotFail && p.getAttackDamage(this, move, !p.battleData.abilityRevealed, false, isCritical).damage >= p.hp; + return doesNotFail && p.getAttackDamage(this, move, !p.battleData.abilityRevealed, false, {isCrit: isCritical, canCrit: true, alwaysCrit: isCritical}).damage >= p.hp; }); }, this); @@ -4812,6 +4891,72 @@ export class EnemyPokemon extends Pokemon { return 0; } + /** + * Calculates how much damage an attack will actually do to a Boss Pokémon. + * + * Returns the normal damage if this Pokémon is not a Boss Pokémon. + * @param damage The damage dealt to the Pokémon + * @returns The actual amount of damage the Pokémon receieves + */ + calculateBossDamage(damage: integer): integer { + if (this.isBoss()) { + const segmentSize = this.getMaxHp() / this.bossSegments; + for (let s = this.bossSegmentIndex; s > 0; s--) { + const hpThreshold = segmentSize * s; + const roundedHpThreshold = Math.round(hpThreshold); + if (this.hp >= roundedHpThreshold) { + if (this.hp - damage <= roundedHpThreshold) { + const hpRemainder = this.hp - roundedHpThreshold; + let segmentsBypassed = 0; + while (segmentsBypassed < this.bossSegmentIndex && this.canBypassBossSegments(segmentsBypassed + 1) && (damage - hpRemainder) >= Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))) { + segmentsBypassed++; + //console.log('damage', damage, 'segment', segmentsBypassed + 1, 'segment size', segmentSize, 'damage needed', Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))); + } + + damage = Utils.toDmgValue(this.hp - hpThreshold + segmentSize * segmentsBypassed); + } + break; + } + } + } + return damage; + } + + /** + * Calculates how many shields will be broken by an attack. + * + * Returns 0 if this Pokémon is not a Boss Pokémon. + * @param damage + * @returns + */ + calculateBossClearedShields(damage: integer): integer { + let clearedBossSegmentIndex = this.isBoss() + ? this.bossSegmentIndex + 1 + : 0; + if (this.isBoss()) { + const segmentSize = this.getMaxHp() / this.bossSegments; + for (let s = this.bossSegmentIndex; s > 0; s--) { + const hpThreshold = segmentSize * s; + const roundedHpThreshold = Math.round(hpThreshold); + if (this.hp >= roundedHpThreshold) { + if (this.hp - damage <= roundedHpThreshold) { + const hpRemainder = this.hp - roundedHpThreshold; + let segmentsBypassed = 0; + while (segmentsBypassed < this.bossSegmentIndex && this.canBypassBossSegments(segmentsBypassed + 1) && (damage - hpRemainder) >= Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))) { + segmentsBypassed++; + //console.log('damage', damage, 'segment', segmentsBypassed + 1, 'segment size', segmentSize, 'damage needed', Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))); + } + + damage = Utils.toDmgValue(this.hp - hpThreshold + segmentSize * segmentsBypassed); + clearedBossSegmentIndex - (s - segmentsBypassed); + } + break; + } + } + } + return clearedBossSegmentIndex; + } + damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { if (this.isFainted()) { return 0; @@ -5097,6 +5242,8 @@ export interface DamageCalculationResult { result: HitResult; /** The damage dealt by the move */ damage: number; + damageLow: number; + damageHigh: number; } /** diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 7a78229cfef..bdc1031b29a 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1819,361 +1819,6 @@ export function setEvioliteOverride(v: string) { evioliteOverride = v; } -export function calculateItemConditions(party: Pokemon[], log?: boolean, showAll?: boolean) { - let total_common = 0 - let total_great = 0 - let total_ultra = 0 - let total_rogue = 0 - let total_master = 0 - let items: string[][] = [[], [], [], [], []] - if (!hasMaximumBalls(party, PokeballType.POKEBALL)) { - items[0].push(`Poké Ball (6)`) - total_common += 6 - } - items[0].push(`Rare Candy (2)`) - total_common += 2 - var potion = Math.min(party.filter(p => (p.getInverseHp() >= 10 || p.getHpRatio() <= 0.875) && !p.isFainted()).length, 3) - if (potion > 0) { - items[0].push(`Potion (${potion * 3})`) - total_common += potion * 3 - } - var superpotion = Math.min(party.filter(p => (p.getInverseHp() >= 25 || p.getHpRatio() <= 0.75) && !p.isFainted()).length, 3) - if (superpotion > 0) { - items[0].push(`Super Potion (${superpotion})`) - total_common += superpotion - } - var ether = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3) - if (ether > 0) { - items[0].push(`Ether (${ether * 3})`) - items[0].push(`Max Ether (${ether})`) - total_common += ether * 4 - } - let lure = skipInLastClassicWaveOrDefault(2)(party) - if (lure > 0) { - items[0].push(`Lure (${lure})`) - total_common += lure; - } - if (showAll) { - items[0].push(`X Attack (0.66)`) - items[0].push(`X Defense (0.66)`) - items[0].push(`X Sp. Atk (0.66)`) - items[0].push(`X Sp. Def (0.66)`) - items[0].push(`X Speed (0.66)`) - items[0].push(`X Accuracy (0.66)`) - } else { - items[0].push(`Any X Item (4, 6 kinds)`) - } - items[0].push(`Berry (2)`) - items[0].push(`Common TM (2)`) - total_common += 8 // X item = 4, berry = 2, common TM = 2 - - - - if (!hasMaximumBalls(party, PokeballType.GREAT_BALL)) { - items[1].push(`Great Ball (6)`) - total_great += 6 - } - items[1].push(`PP Up (2)`) - total_great += 2 - let statusPartyCount = Math.min(party.filter(p => p.hp && !!p.status && !p.getHeldItems().some(i => { - if (i instanceof Modifiers.TurnStatusEffectModifier) { - return (i as Modifiers.TurnStatusEffectModifier).getStatusEffect() === p.status?.effect; - } - return false; - })).length, 3) - if (statusPartyCount > 0) { - items[1].push(`Full Heal (${statusPartyCount * 3})`) - total_great += statusPartyCount * 3 - } - let reviveCount = Math.min(party.filter(p => p.isFainted()).length, 3); - if (reviveCount > 0) { - items[1].push(`Revive (${reviveCount * 9})`) - items[1].push(`Max Revive (${reviveCount * 3})`) - total_great += reviveCount * 12 - } - if (party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2)) { - items[1].push(`Sacred Ash (1)`) - total_great++ - } - let hyperpotion = Math.min(party.filter(p => (p.getInverseHp() >= 100 || p.getHpRatio() <= 0.625) && !p.isFainted()).length, 3) - if (hyperpotion > 0) { - items[1].push(`Hyper Potion (${hyperpotion * 3})`) - total_great += hyperpotion * 3 - } - let maxpotion = Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) - if (maxpotion > 0) { - items[1].push(`Max Potion (${maxpotion})`) - total_great += maxpotion - } - let fullrestore = Math.floor((Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) + statusPartyCount) / 2) - if (fullrestore > 0) { - items[1].push(`Full Restore (${fullrestore})`) - total_great += fullrestore - } - let elexir = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3) - if (elexir) { - items[1].push(`Elexir (${elexir * 3})`) - items[1].push(`Max Elexir (${elexir})`) - total_great += elexir * 4 - } - items[1].push("Dire Hit (4)") - total_great += 4 - let superlure = skipInLastClassicWaveOrDefault(4)(party) - if (superlure > 0) { - items[1].push(`Super Lure (4)`) - items[1].push(`Nugget (5)`) - total_great += 9 - } - let evo = Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8) - if (evo > 0) { - items[1].push(`Evolution Item (${evo})`) - total_great += evo - } - if (party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180) { - if (!party[0].scene.getModifiers(Modifiers.MapModifier).length) { - console.log(`Map (1)`) - } else { - console.log(`Map (1, results in a retry as it's already owned)`) - } - total_great++ - } - items[1].push(`Rare TM (2)`) - total_great += 3 - if (party.find(p => p.getLearnableLevelMoves().length)) { - // Memory Mushroom - let highestLev = party.map(p => p.level).reduce((highestLevel: integer, level: integer) => Math.max(highestLevel, level), 1) - let memoryshroom = Math.min(Math.ceil(highestLev / 20), 4) - if (memoryshroom > 0) { - items[1].push(`Memory Mushroom (${memoryshroom})`) - total_great += memoryshroom - } - } - if (showAll) { - items[1].push(`${i18next.t(`modifierType:BaseStatBoosterItem.${BaseStatBoosterModifierTypeGenerator.items[Stat.HP]}`)} (0.5)`) - items[1].push(`${i18next.t(`modifierType:BaseStatBoosterItem.${BaseStatBoosterModifierTypeGenerator.items[Stat.ATK]}`)} (0.5)`) - items[1].push(`${i18next.t(`modifierType:BaseStatBoosterItem.${BaseStatBoosterModifierTypeGenerator.items[Stat.DEF]}`)} (0.5)`) - items[1].push(`${i18next.t(`modifierType:BaseStatBoosterItem.${BaseStatBoosterModifierTypeGenerator.items[Stat.SPATK]}`)} (0.5)`) - items[1].push(`${i18next.t(`modifierType:BaseStatBoosterItem.${BaseStatBoosterModifierTypeGenerator.items[Stat.SPDEF]}`)} (0.5)`) - items[1].push(`${i18next.t(`modifierType:BaseStatBoosterItem.${BaseStatBoosterModifierTypeGenerator.items[Stat.SPD]}`)} (0.5)`) - } else { - items[1].push(`Any Vitamin (3, 6 kinds)`) - } - total_great += 3 - if (party[0].scene.getModifiers(Modifiers.TerastallizeAccessModifier).length) { - if (showAll) { - const teratypes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - const randomchance1 = 1/3 * 1/64 - const randomchance2 = 1/3 * 63/64 * 1/18 - const teamTypes = party.map(p => p.getTypes(false, false, true)).flat() - teratypes.forEach((v, i) => { - if (i == Type.STELLAR) { - teratypes[i] += randomchance1 - } else { - teratypes[i] += randomchance2 - } - }) - teamTypes.forEach(v => { - teratypes[v] += 2/3 * 1/teamTypes.length - }) - items[1].push(`Any Tera Shard (1, 19 kinds)`) - teratypes.forEach((v, i) => { - items[1].push(` ${i18next.t(`pokemonInfo:Type.${Type[i]}`)}: ${Math.round(v*1000)/10}%`) - }) - } else { - items[1].push(`Any Tera Shard (1, 19 kinds)`) - } - total_great++; - } - if (party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1) { - items[1].push(`DNA Splicer (4)`) - total_great += 4 - } - if (!party[0].scene.gameMode.isDaily ) { - items[1].push("Voucher (1, or 0 if reroll)") - total_great += 1 - } - - - - if (!hasMaximumBalls(party, PokeballType.ULTRA_BALL)) { - items[2].push(`Ultra Ball (15)`) - total_ultra += 15 - } - if (superlure) { - items[2].push(`Max Lure (4)`) - items[2].push(`Big Nugget (12)`) - total_ultra += 16 - } - items[2].push(`PP Max (3)`) - items[2].push(`Mint (4)`) - total_ultra += 7 - let evoRare = Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15) * 4, 32) - if (evoRare) { - items[2].push(`Rare Evolution Item (${evoRare})`) - total_ultra += evoRare - } - if (superlure) { - items[2].push(`Amulet Coin (3)`) - total_ultra += 3 - } - if (!party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.unlocks[Unlockables.EVIOLITE]) { - if (party.some(p => ((p.getSpeciesForm(true).speciesId in pokemonEvolutions) || (p.isFusion() && (p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions))) && !p.getHeldItems().some(i => i instanceof Modifiers.EvolutionStatBoosterModifier))) { - items[2].push(`Eviolite (10)`) - total_ultra += 10 - } - } - items[2].push(`Species Stat Booster (12, retries if incompatible)`) - total_ultra += 12 - const checkedSpecies = [ Species.FARFETCHD, Species.GALAR_FARFETCHD, Species.SIRFETCHD ] - const checkedAbilitiesT = [Abilities.QUICK_FEET, Abilities.GUTS, Abilities.MARVEL_SCALE, Abilities.TOXIC_BOOST, Abilities.POISON_HEAL, Abilities.MAGIC_GUARD]; - const checkedAbilitiesF = [Abilities.QUICK_FEET, Abilities.GUTS, Abilities.MARVEL_SCALE, Abilities.FLARE_BOOST, Abilities.MAGIC_GUARD]; - const checkedAbilitiesW = [Abilities.WEAK_ARMOR, Abilities.CONTRARY, Abilities.MOODY, Abilities.ANGER_SHELL, Abilities.COMPETITIVE, Abilities.DEFIANT]; - const checkedMoves = [Moves.FACADE, Moves.TRICK, Moves.FLING, Moves.SWITCHEROO, Moves.PSYCHO_SHIFT]; - const weightMultiplier = party.filter( - p => !p.getHeldItems().some(i => i instanceof Modifiers.ResetNegativeStatStageModifier && i.stackCount >= i.getMaxHeldItemCount(p)) && - (checkedAbilitiesW.some(a => p.hasAbility(a, false, true)) || p.getMoveset(true).some(m => m && selfStatLowerMoves.includes(m.moveId)))).length; - if (party.some(p => !p.getHeldItems().some(i => i instanceof Modifiers.SpeciesCritBoosterModifier) && (checkedSpecies.includes(p.getSpeciesForm(true).speciesId) || (p.isFusion() && checkedSpecies.includes(p.getFusionSpeciesForm(true).speciesId))))) { - items[2].push(`Leek (12)`) - total_ultra += 12 - } - if (party.some(p => !p.getHeldItems().some(i => i instanceof Modifiers.TurnStatusEffectModifier) && (checkedAbilitiesT.some(a => p.hasAbility(a, false, true)) || p.getMoveset(true).some(m => m && checkedMoves.includes(m.moveId))))) { - items[2].push(`Toxic Orb (10)`) - total_ultra += 10 - } - if (party.some(p => !p.getHeldItems().some(i => i instanceof Modifiers.TurnStatusEffectModifier) && (checkedAbilitiesF.some(a => p.hasAbility(a, false, true)) || p.getMoveset(true).some(m => m && checkedMoves.includes(m.moveId))))) { - items[2].push(`Flame Orb (10)`) - total_ultra += 10 - } - let whiteherb = 0 * (weightMultiplier ? 2 : 1) + (weightMultiplier ? weightMultiplier * 0 : 0) - if (whiteherb) { - items[2].push(`White Herb (${whiteherb})`) - total_ultra += whiteherb - } - if (superlure) { - items[2].push(`Wide Lens (5)`) - total_ultra += 5 - } - items[2].push(`Reviver Seed (4)`) - items[2].push(`Attack Type Booster (9)`) - items[2].push(`Epic TM (11)`) - items[2].push(`Rarer Candy (4)`) - if (superlure) { - items[2].push(`Golden Punch (2)`) - items[2].push(`IV Scanner (4)`) - items[2].push(`EXP Charm (8)`) - items[2].push(`EXP Share (10)`) - items[2].push(`EXP Balance (3)`) - total_ultra += 27 - } - let teraorb = Math.min(Math.max(Math.floor(party[0].scene.currentBattle.waveIndex / 50) * 2, 1), 4) - if (teraorb) { - items[2].push(`Tera Orb (${teraorb})`) - total_ultra += teraorb - } - items[2].push(`Quick Claw (3)`) - items[2].push(`Wide Lens (4)`) - total_ultra += 35 - - - - if (!hasMaximumBalls(party, PokeballType.ROGUE_BALL)) { - items[3].push(`Rogue Ball (16)`) - total_rogue += 16 - } - if (superlure) { - items[3].push(`Relic Gold (2)`) - total_rogue += 2 - } - items[3].push(`Leftovers (3)`) - items[3].push(`Shell Bell (3)`) - items[3].push(`Berry Pouch (4)`) - items[3].push(`Grip Claw (5)`) - items[3].push(`Scope Lens (4)`) - items[3].push(`Baton (2)`) - items[3].push(`Soul Dew (7)`) - items[3].push(`Soothe Bell (4)`) - let abilitycharm = skipInClassicAfterWave(189, 6)(party); - if (abilitycharm) { - items[3].push(`Ability Charm (${abilitycharm})`) - total_rogue += abilitycharm - } - items[3].push(`Focus Band (5)`) - items[3].push(`King's Rock (3)`) - total_rogue += 40 - if (superlure) { - items[3].push(`Lock Capsule (3)`) - items[3].push(`Super EXP Charm (8)`) - total_rogue += 11 - } - let formchanger = Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 6 - let megabraclet = Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9 - let dynamaxband = Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9 - if (formchanger) { - items[3].push(`Form Change Item (${formchanger}, retries if incompatible)`) - total_rogue += formchanger - } - if (megabraclet) { - items[3].push(`Mega Bracelet (${megabraclet}, retries if already owned)`) - total_rogue += megabraclet - } - if (dynamaxband) { - items[3].push(`Dynamax Band (${dynamaxband}, retries if already owned)`) - total_rogue += dynamaxband - } - if (!party[0].scene.gameMode.isDaily) { - items[3].push(`Voucher Plus (3 - number of rerolls)`) - total_rogue += 3 - } - - - - if (!hasMaximumBalls(party, PokeballType.MASTER_BALL)) { - items[4].push(`Master Ball (24)`) - total_master += 24 - } - items[4].push(`Shiny Charm (14)`) - total_master += 14 - items[4].push(`Healing Charm (18)`) - total_master += 18 - items[4].push(`Multi Lens (18)`) - total_master += 18 - if (!party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly) { - items[4].push(`Voucher Premium (5, -2 per reroll)`) - total_master += 3 - } - if (!party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1) { - items[4].push(`DNA Splicer (24)`) - total_master += 24 - } - if ((!party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE])) { - items[4].push(`Mini Black Hole (1)`) - total_master += 1 - } - - - - items[0].sort() - items[1].sort() - items[2].sort() - items[3].sort() - items[4].sort() - if (!log) - return items; - let itemlabels = [ - `Poké (${items[0].length}, weight ${total_common})`, - `Great (${items[1].length}, weight ${total_great})`, - `Ultra (${items[2].length}, weight ${total_ultra})`, - `Rogue (${items[3].length}, weight ${total_rogue})`, - `Master (${items[4].length}, weight ${total_master})` - ] - items.forEach((mi, idx) => { - console.log(itemlabels[idx]) - mi.forEach(m => { - console.log(" " + mi) - }) - }) - return items; -} const modifierPool: ModifierPool = { [ModifierTier.COMMON]: [ diff --git a/src/old-phases.ts b/src/old-phases.ts deleted file mode 100644 index b359fd1e6cb..00000000000 --- a/src/old-phases.ts +++ /dev/null @@ -1,7825 +0,0 @@ -//#region 00 Imports -import BattleScene, { bypassLogin } from "./battle-scene"; -import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon"; -import * as Utils from "./utils"; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr, MoveHeaderAttr, MoveCategory } from "./data/move"; -import { Mode } from "./ui/ui"; -import { Command } from "./ui/command-ui-handler"; -import { Stat } from "./data/pokemon-stat"; -import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier, PokemonResetNegativeStatStageModifier, PersistentModifier } from "./modifier/modifier"; -import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler"; -import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./data/pokeball"; -import { CommonAnim, CommonBattleAnim, MoveAnim, initMoveAnim, loadMoveAnimAssets } from "./data/battle-anims"; -import { StatusEffect, getStatusEffectActivationText, getStatusEffectCatchRateMultiplier, getStatusEffectHealText, getStatusEffectObtainText, getStatusEffectOverlapText } from "./data/status-effect"; -import { SummaryUiMode } from "./ui/summary-ui-handler"; -import EvolutionSceneHandler from "./ui/evolution-scene-handler"; -import { EvolutionPhase } from "./evolution-phase"; -import { Phase } from "./phase"; -import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "./data/battle-stat"; -import { biomeLinks, getBiomeName } from "./data/biomes"; -import { ModifierTier } from "./modifier/modifier-tier"; -import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, calculateItemConditions, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getLuckString, getModifierType, getPartyLuckValue, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds, setEvioliteOverride } from "./modifier/modifier-type"; -import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, TrappedTag } from "./data/battler-tags"; -import { getPokemonNameWithAffix } from "./messages"; -import { Starter } from "./ui/starter-select-ui-handler"; -import { Gender } from "./data/gender"; -import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; -import { ArenaTagSide, ArenaTrapTag, ConditionalProtectTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, ChangeMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, PreventBypassSpeedChanceAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr, ReduceBurnDamageAbAttr, BattlerTagImmunityAbAttr } from "./data/ability"; -import { Unlockables, getUnlockableName } from "./system/unlockables"; -import { getBiomeKey } from "./field/arena"; -import { BattleType, BattlerIndex, TurnCommand } from "./battle"; -import { ChallengeAchv, HealAchv, LevelAchv, achvs } from "./system/achv"; -import { TrainerSlot, trainerConfigs } from "./data/trainer-config"; -import { EggHatchPhase } from "./egg-hatch-phase"; -import { Egg } from "./data/egg"; -import { vouchers } from "./system/voucher"; -import { clientSessionId, loggedInUser, updateUserInfo } from "./account"; -import { SessionSaveData, decrypt } from "./system/game-data"; -import { addPokeballCaptureStars, addPokeballOpenParticles } from "./field/anims"; -import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms"; -import { battleSpecDialogue, getCharVariantFromDialogue, miscDialogue } from "./data/dialogue"; -import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "./ui/modifier-select-ui-handler"; -import { SettingKeys } from "./system/settings/settings"; -import { Tutorial, handleTutorial } from "./tutorial"; -import { TerrainType } from "./data/terrain"; -import { OptionSelectConfig, OptionSelectItem } from "./ui/abstact-option-select-ui-handler"; -import { SaveSlotUiMode } from "./ui/save-slot-select-ui-handler"; -import { fetchDailyRunSeed, getDailyRunStarters } from "./data/daily-run"; -import { GameMode, GameModes, getGameMode } from "./game-mode"; -import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./data/pokemon-species"; -import i18next from "./plugins/i18n"; -import Overrides from "#app/overrides"; -import { TextStyle, addTextObject, getTextColor } from "./ui/text"; -import { Type } from "./data/type"; -import { BerryUsedEvent, EncounterPhaseEvent, MoveUsedEvent, TurnEndEvent, TurnInitEvent } from "./events/battle-scene"; -import { Abilities } from "#enums/abilities"; -import { ArenaTagType } from "#enums/arena-tag-type"; -import { BattleSpec } from "#enums/battle-spec"; -import { BattleStyle } from "#enums/battle-style"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { Biome } from "#enums/biome"; -import { ExpNotification } from "#enums/exp-notification"; -import { Moves } from "#enums/moves"; -import { PlayerGender } from "#enums/player-gender"; -import { Species } from "#enums/species"; -import { TrainerType } from "#enums/trainer-type"; -import TrainerData from "./system/trainer-data"; -import PersistentModifierData from "./system/modifier-data"; -import ArenaData from "./system/arena-data"; -import ChallengeData from "./system/challenge-data"; -import { Challenges } from "./enums/challenges" -import PokemonData from "./system/pokemon-data" -import * as LoggerTools from "./logger" -import { applyChallenges, ChallengeType } from "./data/challenge"; -import { pokemonEvolutions } from "./data/pokemon-evolutions"; -import { getNatureDecrease, getNatureIncrease, getNatureName } from "./data/nature"; -import { GameDataType } from "./enums/game-data-type"; -import { Session } from "inspector"; - -const { t } = i18next; - - - -//#endregion - - - - - -//#region 01 Uncategorized -function catchCalc(pokemon: EnemyPokemon) { - const _3m = 3 * pokemon.getMaxHp(); - const _2h = 2 * pokemon.hp; - const catchRate = pokemon.species.catchRate; - const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; - const rate1 = Math.round(65536 / Math.sqrt(Math.sqrt(255 / (Math.round((((_3m - _2h) * catchRate * 1) / _3m) * statusMultiplier))))); - const rate2 = Math.round(65536 / Math.sqrt(Math.sqrt(255 / (Math.round((((_3m - _2h) * catchRate * 1.5) / _3m) * statusMultiplier))))); - const rate3 = Math.round(65536 / Math.sqrt(Math.sqrt(255 / (Math.round((((_3m - _2h) * catchRate * 2) / _3m) * statusMultiplier))))); - const rate4 = Math.round(65536 / Math.sqrt(Math.sqrt(255 / (Math.round((((_3m - _2h) * catchRate * 3) / _3m) * statusMultiplier))))); - - var rates = [rate1, rate2, rate3, rate4] - var rates2 = rates.map(r => ((r/65536) ** 3)) - //console.log(rates2) - - return rates2 -} -function catchCalcRaw(pokemon: EnemyPokemon) { - const _3m = 3 * pokemon.getMaxHp(); - const _2h = 2 * pokemon.hp; - const catchRate = pokemon.species.catchRate; - const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; - const rate1 = Math.round(65536 / Math.sqrt(Math.sqrt(255 / (Math.round((((_3m - _2h) * catchRate * 1) / _3m) * statusMultiplier))))); - const rate2 = Math.round(65536 / Math.sqrt(Math.sqrt(255 / (Math.round((((_3m - _2h) * catchRate * 1.5) / _3m) * statusMultiplier))))); - const rate3 = Math.round(65536 / Math.sqrt(Math.sqrt(255 / (Math.round((((_3m - _2h) * catchRate * 2) / _3m) * statusMultiplier))))); - const rate4 = Math.round(65536 / Math.sqrt(Math.sqrt(255 / (Math.round((((_3m - _2h) * catchRate * 3) / _3m) * statusMultiplier))))); - - var rates = [rate1, rate2, rate3, rate4] - var rates2 = rates.map(r => ((r/65536) ** 3)) - //console.log(rates2) - //console.log("output: ", rates) - - return rates -} - -/** - * Finds the best Poké Ball to catch a Pokemon with, and the % chance of capturing it. - * @param pokemon The Pokémon to get the catch rate for. - * @param override Show the best Poké Ball to use, even if you don't have any. - * @returns The name and % rate of the best Poké Ball. - */ -export function findBest(scene: BattleScene, pokemon: EnemyPokemon, override?: boolean) { - var rates = catchCalc(pokemon) - var rates_raw = catchCalcRaw(pokemon) - var rolls = [] - var offset = 0 - scene.getModifiers(BypassSpeedChanceModifier, true).forEach(m => { - //console.log(m, m.getPokemon(this.scene), pokemon) - var p = m.getPokemon(scene) - scene.getField().forEach((p2, idx) => { - if (p == p2) { - console.log(m.getPokemon(scene)?.name + " (Position: " + (idx + 1) + ") has a Quick Claw") - offset++ - } - }) - }) - scene.currentBattle.multiInt(scene, rolls, offset + 3, 65536, undefined, "Catch prediction") - //console.log(rolls) - //console.log(rolls.slice(offset, offset + 3)) - if (scene.pokeballCounts[0] == 0 && !override) rates[0] = 0 - if (scene.pokeballCounts[1] == 0 && !override) rates[1] = 0 - if (scene.pokeballCounts[2] == 0 && !override) rates[2] = 0 - if (scene.pokeballCounts[3] == 0 && !override) rates[3] = 0 - var rates2 = rates.slice() - rates2.sort(function(a, b) {return b - a}) - const ballNames = [ - "Poké Ball", - "Great Ball", - "Ultra Ball", - "Rogue Ball", - "Master Ball" - ] - var func_output = "" - rates_raw.forEach((v, i) => { - if (scene.pokeballCounts[i] == 0 && !override) - return; // Don't list success for Poke Balls we don't have - //console.log(ballNames[i]) - //console.log(v, rolls[offset + 0], v > rolls[offset + 0]) - //console.log(v, rolls[offset + 1], v > rolls[offset + 1]) - //console.log(v, rolls[offset + 2], v > rolls[offset + 2]) - if (v > rolls[offset + 0]) { - //console.log("1 roll") - if (v > rolls[offset + 1]) { - //console.log("2 roll") - if (v > rolls[offset + 2]) { - //console.log("Caught!") - if (func_output == "") { - func_output = ballNames[i] + " catches" - } - } - } - } - if (v > rolls[offset] && v > rolls[1 + offset] && v > rolls[2 + offset]) { - if (func_output == "") { - func_output = ballNames[i] + " catches" - } - } - }) - if (func_output != "") { - return func_output - } - return "Can't catch" - var n = "" - switch (rates2[0]) { - case rates[0]: - // Poke Balls are best - n = "Poké Ball " - break; - case rates[1]: - // Great Balls are best - n = "Great Ball " - break; - case rates[2]: - // Ultra Balls are best - n = "Ultra Ball " - break; - case rates[3]: - // Rogue Balls are best - n = "Rogue Ball " - break; - default: - // Master Balls are the only thing that will work - if (scene.pokeballCounts[4] != 0 || override) { - return "Master Ball"; - } else { - return "No balls" - } - } - return n + " (FAIL)" - return n + Math.round(rates2[0] * 100) + "%"; -} -export function parseSlotData(slotId: integer): SessionSaveData | undefined { - var S = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`) - if (S == null) { - // No data in this slot - return undefined; - } - var dataStr = decrypt(S, true) - var Save = JSON.parse(dataStr, (k: string, v: any) => { - /*const versions = [ scene.game.config.gameVersion, sessionData.gameVersion || '0.0.0' ]; - - if (versions[0] !== versions[1]) { - const [ versionNumbers, oldVersionNumbers ] = versions.map(ver => ver.split('.').map(v => parseInt(v))); - }*/ - - if (k === "party" || k === "enemyParty") { - const ret: PokemonData[] = []; - if (v === null) { - v = []; - } - for (const pd of v) { - ret.push(new PokemonData(pd)); - } - return ret; - } - - if (k === "trainer") { - return v ? new TrainerData(v) : null; - } - - if (k === "modifiers" || k === "enemyModifiers") { - const player = k === "modifiers"; - const ret: PersistentModifierData[] = []; - if (v === null) { - v = []; - } - for (const md of v) { - if (md?.className === "ExpBalanceModifier") { // Temporarily limit EXP Balance until it gets reworked - md.stackCount = Math.min(md.stackCount, 4); - } - if (md instanceof EnemyAttackStatusEffectChanceModifier && md.effect === StatusEffect.FREEZE || md.effect === StatusEffect.SLEEP) { - continue; - } - ret.push(new PersistentModifierData(md, player)); - } - return ret; - } - - if (k === "arena") { - return new ArenaData(v); - } - - if (k === "challenges") { - const ret: ChallengeData[] = []; - if (v === null) { - v = []; - } - for (const c of v) { - ret.push(new ChallengeData(c)); - } - return ret; - } - - return v; - }) as SessionSaveData; - Save.slot = slotId - Save.description = (slotId + 1) + " - " - var challengeParts: ChallengeData[] | undefined[] = new Array(5) - var nameParts: string[] | undefined[] = new Array(5) - if (Save.challenges != undefined) { - for (var i = 0; i < Save.challenges.length; i++) { - switch (Save.challenges[i].id) { - case Challenges.SINGLE_TYPE: - challengeParts[0] = Save.challenges[i] - nameParts[1] = Save.challenges[i].toChallenge().getValue() - nameParts[1] = nameParts[1][0].toUpperCase() + nameParts[1].substring(1) - if (nameParts[1] == "unknown") { - nameParts[1] = undefined - challengeParts[1] = undefined - } - break; - case Challenges.SINGLE_GENERATION: - challengeParts[1] = Save.challenges[i] - nameParts[0] = "Gen " + Save.challenges[i].value - if (nameParts[0] == "Gen 0") { - nameParts[0] = undefined - challengeParts[0] = undefined - } - break; - case Challenges.LOWER_MAX_STARTER_COST: - challengeParts[2] = Save.challenges[i] - nameParts[3] = (10 - challengeParts[0]!.value) + "cost" - break; - case Challenges.LOWER_STARTER_POINTS: - challengeParts[3] = Save.challenges[i] - nameParts[4] = (10 - challengeParts[0]!.value) + "pt" - break; - case Challenges.FRESH_START: - challengeParts[4] = Save.challenges[i] - nameParts[2] = "FS" - break; - } - } - } - for (var i = 0; i < challengeParts.length; i++) { - if (challengeParts[i] == undefined || challengeParts[i] == null) { - challengeParts.splice(i, 1) - i-- - } - } - for (var i = 0; i < nameParts.length; i++) { - if (nameParts[i] == undefined || nameParts[i] == null || nameParts[i] == "") { - nameParts.splice(i, 1) - i-- - } - } - if (challengeParts.length == 1 && false) { - switch (challengeParts[0]!.id) { - case Challenges.SINGLE_TYPE: - Save.description += "Mono " + challengeParts[0]!.toChallenge().getValue() - break; - case Challenges.SINGLE_GENERATION: - Save.description += "Gen " + challengeParts[0]!.value - break; - case Challenges.LOWER_MAX_STARTER_COST: - Save.description += "Max cost " + (10 - challengeParts[0]!.value) - break; - case Challenges.LOWER_STARTER_POINTS: - Save.description += (10 - challengeParts[0]!.value) + "-point" - break; - case Challenges.FRESH_START: - Save.description += "Fresh Start" - break; - } - } else if (challengeParts.length == 0) { - switch (Save.gameMode) { - case GameModes.CLASSIC: - Save.description += "Classic"; - break; - case GameModes.ENDLESS: - Save.description += "Endless"; - break; - case GameModes.SPLICED_ENDLESS: - Save.description += "Endless+"; - break; - case GameModes.DAILY: - Save.description += "Daily"; - break; - } - } else { - Save.description += nameParts.join(" ") - } - Save.description += " (" + getBiomeName(Save.arena.biome) + " " + Save.waveIndex + ")" - return Save; -} -//#endregion - - - - - -//#region 02 LoginPhase -export class LoginPhase extends Phase { - private showText: boolean; - - constructor(scene: BattleScene, showText?: boolean) { - super(scene); - - this.showText = showText === undefined || !!showText; - } - - start(): void { - super.start(); - - const hasSession = !!Utils.getCookie(Utils.sessionIdKey); - - this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] }); - Utils.executeIf(bypassLogin || hasSession, updateUserInfo).then(response => { - const success = response ? response[0] : false; - const statusCode = response ? response[1] : null; - if (!success) { - if (!statusCode || statusCode === 400) { - if (this.showText) { - this.scene.ui.showText(i18next.t("menu:logInOrCreateAccount")); - } - - this.scene.playSound("menu_open"); - - const loadData = () => { - updateUserInfo().then(success => { - if (!success[0]) { - Utils.removeCookie(Utils.sessionIdKey); - this.scene.reset(true, true); - return; - } - this.scene.gameData.loadSystem().then(() => this.end()); - }); - }; - - this.scene.ui.setMode(Mode.LOGIN_FORM, { - buttonActions: [ - () => { - this.scene.ui.playSelect(); - loadData(); - }, () => { - this.scene.playSound("menu_open"); - this.scene.ui.setMode(Mode.REGISTRATION_FORM, { - buttonActions: [ - () => { - this.scene.ui.playSelect(); - updateUserInfo().then(success => { - if (!success[0]) { - Utils.removeCookie(Utils.sessionIdKey); - this.scene.reset(true, true); - return; - } - this.end(); - } ); - }, () => { - this.scene.unshiftPhase(new LoginPhase(this.scene, false)); - this.end(); - } - ] - }); - }, () => { - const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`); - const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID; - const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`; - window.open(discordUrl, "_self"); - }, () => { - const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`); - const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID; - const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`; - window.open(googleUrl, "_self"); - } - ] - }); - } else if (statusCode === 401) { - Utils.removeCookie(Utils.sessionIdKey); - this.scene.reset(true, true); - } else { - this.scene.unshiftPhase(new UnavailablePhase(this.scene)); - super.end(); - } - return null; - } else { - this.scene.gameData.loadSystem().then(success => { - if (success || bypassLogin) { - this.end(); - } else { - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(t("menu:failedToLoadSaveData")); - } - }); - } - }); - } - - end(): void { - this.scene.ui.setMode(Mode.MESSAGE); - - if (!this.scene.gameData.gender) { - this.scene.unshiftPhase(new SelectGenderPhase(this.scene)); - } - - handleTutorial(this.scene, Tutorial.Intro).then(() => super.end()); - } -} -//#endregion - - - - - -//#region 03 TitlePhase -export class TitlePhase extends Phase { - private loaded: boolean; - private lastSessionData: SessionSaveData; - public gameMode: GameModes; - - constructor(scene: BattleScene) { - super(scene); - - this.loaded = false; - } - - setBiomeByType(biome: Biome, override?: boolean): void { - if (!this.scene.menuChangesBiome && !override) - return; - this.scene.arenaBg.setTexture(`${getBiomeKey(biome)}_bg`); - } - setBiomeByName(biome: string, override?: boolean): void { - if (!this.scene.menuChangesBiome && !override) - return; - this.scene.arenaBg.setTexture(`${getBiomeKey(Utils.getEnumValues(Biome)[Utils.getEnumKeys(Biome).indexOf(biome)])}_bg`); - } - setBiomeByFile(sessionData: SessionSaveData, override?: boolean): void { - if (!this.scene.menuChangesBiome && !override) - return; - this.scene.arenaBg.setTexture(`${getBiomeKey(sessionData.arena.biome)}_bg`); - } - - confirmSlot = (message: string, slotFilter: (i: integer) => boolean, callback: (i: integer) => void) => { - const p = this; - this.scene.ui.revertMode(); - this.scene.ui.showText(message, null, () => { - const config: OptionSelectConfig = { - options: new Array(5).fill(null).map((_, i) => i).filter(slotFilter).map(i => { - var data = parseSlotData(i) - return { - //label: `${i18next.t("menuUiHandler:slot", {slotNumber: i+1})}`, - label: (data ? `${i18next.t("menuUiHandler:slot", {slotNumber: i+1})}${data.description.substring(1)}` : `${i18next.t("menuUiHandler:slot", {slotNumber: i+1})}`), - handler: () => { - callback(i); - this.scene.ui.revertMode(); - this.scene.ui.showText("", 0); - return true; - } - }; - }).concat([{ - label: i18next.t("menuUiHandler:cancel"), - handler: () => { - p.callEnd() - return true - } - }]), - //xOffset: 98 - }; - this.scene.ui.setOverlayMode(Mode.MENU_OPTION_SELECT, config); - }); - }; - - start(): void { - super.start(); - //console.log(LoggerTools.importDocument(JSON.stringify(LoggerTools.newDocument()))) - - this.scene.ui.clearText(); - this.scene.ui.fadeIn(250); - - this.scene.playBgm("title", true); - - this.scene.biomeChangeMode = false - - this.scene.gameData.getSession(loggedInUser?.lastSessionSlot ?? -1).then(sessionData => { - if (sessionData) { - this.lastSessionData = sessionData; - this.setBiomeByFile(sessionData, true) - this.setBiomeByType(Biome.END) - } - this.showOptions(); - }).catch(err => { - console.error(err); - this.showOptions(); - }); - } - - getLastSave(log?: boolean, dailyOnly?: boolean, noDaily?: boolean): SessionSaveData | undefined { - var saves: Array> = []; - for (var i = 0; i < 5; i++) { - var s = parseSlotData(i); - if (s != undefined) { - if ((!noDaily && !dailyOnly) || (s.gameMode == GameModes.DAILY && dailyOnly) || (s.gameMode != GameModes.DAILY && noDaily)) { - saves.push([i, s, s.timestamp]); - } - } - } - saves.sort((a, b): integer => {return b[2] - a[2]}) - if (log) console.log(saves) - if (saves == undefined) return undefined; - if (saves[0] == undefined) return undefined; - return saves[0][1] - } - getLastSavesOfEach(log?: boolean): SessionSaveData[] | undefined { - var saves: Array> = []; - for (var i = 0; i < 5; i++) { - var s = parseSlotData(i); - if (s != undefined) { - saves.push([i, s, s.timestamp]); - } - } - saves.sort((a, b): integer => {return (b[2] as number) - (a[2] as number)}) - if (log) console.log(saves) - if (saves == undefined) return undefined; - if (saves[0] == undefined) return undefined; - var validSaves: Array> = [] - var hasNormal = false; - var hasDaily = false; - for (var i = 0; i < saves.length; i++) { - if ((saves[i][1] as SessionSaveData).gameMode == GameModes.DAILY && !hasDaily) { - hasDaily = true; - validSaves.push(saves[i]) - } - if ((saves[i][1] as SessionSaveData).gameMode != GameModes.DAILY && !hasNormal) { - hasNormal = true; - validSaves.push(saves[i]) - } - } - console.log(saves, validSaves) - if (validSaves.length == 0) - return undefined; - return validSaves.map(f => f[1] as SessionSaveData); - } - getSaves(log?: boolean, dailyOnly?: boolean): SessionSaveData[] | undefined { - var saves: Array> = []; - for (var i = 0; i < 5; i++) { - var s = parseSlotData(i); - if (s != undefined) { - if (!dailyOnly || s.gameMode == GameModes.DAILY) { - saves.push([i, s, s.timestamp]); - } - } - } - saves.sort((a, b): integer => {return b[2] - a[2]}) - if (log) console.log(saves) - if (saves == undefined) return undefined; - return saves.map(f => f[1]); - } - getSavesUnsorted(log?: boolean, dailyOnly?: boolean): SessionSaveData[] | undefined { - var saves: Array> = []; - for (var i = 0; i < 5; i++) { - var s = parseSlotData(i); - if (s != undefined) { - if (!dailyOnly || s.gameMode == GameModes.DAILY) { - saves.push([i, s, s.timestamp]); - } - } - } - if (log) console.log(saves) - if (saves == undefined) return undefined; - return saves.map(f => f[1]); - } - - callEnd(): boolean { - this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); - super.end(); - return true; - } - - showLoggerOptions(txt: string, options: OptionSelectItem[]): boolean { - this.scene.ui.showText("Export or clear game logs.", null, () => this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, { options: options })); - return true; - } - - logMenu(): boolean { - const options: OptionSelectItem[] = []; - LoggerTools.getLogs() - for (var i = 0; i < LoggerTools.logs.length; i++) { - if (localStorage.getItem(LoggerTools.logs[i][1]) != null) { - options.push(LoggerTools.generateOption(i, this.getSaves()) as OptionSelectItem) - } else { - //options.push(LoggerTools.generateAddOption(i, this.scene, this)) - } - } - options.push({ - label: "Delete all", - handler: () => { - for (var i = 0; i < LoggerTools.logs.length; i++) { - if (localStorage.getItem(LoggerTools.logs[i][1]) != null) { - localStorage.removeItem(LoggerTools.logs[i][1]) - } - } - this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); - super.end(); - return true; - } - }, { - label: i18next.t("menu:cancel"), - handler: () => { - this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); - super.end(); - return true; - } - }); - this.scene.ui.showText("Export or clear game logs.", null, () => this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, { options: options })); - return true; - } - logRenameMenu(): boolean { - const options: OptionSelectItem[] = []; - LoggerTools.getLogs() - this.setBiomeByType(Biome.FACTORY) - for (var i = 0; i < LoggerTools.logs.length; i++) { - if (localStorage.getItem(LoggerTools.logs[i][1]) != null) { - options.push(LoggerTools.generateEditOption(this.scene, i, this.getSaves(), this) as OptionSelectItem) - } else { - //options.push(LoggerTools.generateAddOption(i, this.scene, this)) - } - } - options.push({ - label: "Delete all", - handler: () => { - for (var i = 0; i < LoggerTools.logs.length; i++) { - if (localStorage.getItem(LoggerTools.logs[i][1]) != null) { - localStorage.removeItem(LoggerTools.logs[i][1]) - } - } - this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); - super.end(); - return true; - } - }, { - label: i18next.t("menu:cancel"), - handler: () => { - this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); - super.end(); - return true; - } - }); - this.scene.ui.showText("Export, rename, or delete logs.", null, () => this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, { options: options })); - return true; - } - - showOptions(): void { - this.scene.biomeChangeMode = true - const options: OptionSelectItem[] = []; - if (false) - if (loggedInUser && loggedInUser!.lastSessionSlot > -1) { - options.push({ - label: i18next.t("continue", {ns: "menu"}), - handler: () => { - this.loadSaveSlot(this.lastSessionData ? -1 : loggedInUser!.lastSessionSlot); - return true; - } - }); - } - // Replaces 'Continue' with all Daily Run saves, sorted by when they last saved - // If there are no daily runs, it instead shows the most recently saved run - // If this fails too, there are no saves, and the option does not appear - var lastsaves = this.getSaves(false, true); // Gets all Daily Runs sorted by last play time - var lastsave = this.getLastSave(); // Gets the last save you played - var ls1 = this.getLastSave(false, true) - var ls2 = this.getLastSavesOfEach() - this.scene.quickloadDisplayMode = "Both" - switch (true) { - case (this.scene.quickloadDisplayMode == "Daily" && ls1 != undefined): - options.push({ - label: (ls1.description ? ls1.description : "[???]"), - handler: () => { - this.loadSaveSlot(ls1!.slot); - return true; - } - }) - break; - case this.scene.quickloadDisplayMode == "Dailies" && lastsaves != undefined && ls1 != undefined: - lastsaves.forEach(lastsave1 => { - options.push({ - label: (lastsave1.description ? lastsave1.description : "[???]"), - handler: () => { - this.loadSaveSlot(lastsave1.slot); - return true; - } - }) - }) - break; - case lastsave != undefined && (this.scene.quickloadDisplayMode == "Latest" || ((this.scene.quickloadDisplayMode == "Daily" || this.scene.quickloadDisplayMode == "Dailies") && ls1 == undefined)): - options.push({ - label: (lastsave.description ? lastsave.description : "[???]"), - handler: () => { - this.loadSaveSlot(lastsave!.slot); - return true; - } - }) - break; - case this.scene.quickloadDisplayMode == "Both" && ls2 != undefined: - ls2.forEach(lastsave2 => { - options.push({ - label: (lastsave2.description ? lastsave2.description : "[???]"), - handler: () => { - this.loadSaveSlot(lastsave2.slot); - return true; - } - }) - }) - break; - default: // If set to "Off" or all above conditions failed - if (loggedInUser && loggedInUser.lastSessionSlot > -1) { - options.push({ - label: i18next.t("continue", { ns: "menu"}), - handler: () => { - this.loadSaveSlot(this.lastSessionData ? -1 : loggedInUser!.lastSessionSlot); - return true; - } - }); - } - break; - } - options.push({ - label: i18next.t("menu:newGame"), - handler: () => { - this.scene.biomeChangeMode = false - this.setBiomeByType(Biome.TOWN) - const setModeAndEnd = (gameMode: GameModes) => { - this.gameMode = gameMode; - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.clearText(); - this.end(); - }; - if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { - const options: OptionSelectItem[] = [ - { - label: GameMode.getModeName(GameModes.CLASSIC), - handler: () => { - setModeAndEnd(GameModes.CLASSIC); - return true; - } - }, - { - label: GameMode.getModeName(GameModes.CHALLENGE), - handler: () => { - setModeAndEnd(GameModes.CHALLENGE); - return true; - } - }, - { - label: GameMode.getModeName(GameModes.ENDLESS), - handler: () => { - setModeAndEnd(GameModes.ENDLESS); - return true; - } - } - ]; - if (this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { - options.push({ - label: GameMode.getModeName(GameModes.SPLICED_ENDLESS), - handler: () => { - setModeAndEnd(GameModes.SPLICED_ENDLESS); - return true; - } - }); - } - options.push({ - label: i18next.t("menuUiHandler:importSession"), - handler: () => { - this.confirmSlot(i18next.t("menuUiHandler:importSlotSelect"), () => true, slotId => this.scene.gameData.importData(GameDataType.SESSION, slotId)); - return true; - }, - keepOpen: true - }) - options.push({ - label: i18next.t("menu:cancel"), - handler: () => { - this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); - super.end(); - return true; - } - }); - this.scene.ui.showText(i18next.t("menu:selectGameMode"), null, () => this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, { options: options })); - } else { - const options: OptionSelectItem[] = [ - { - label: GameMode.getModeName(GameModes.CLASSIC), - handler: () => { - setModeAndEnd(GameModes.CLASSIC); - return true; - } - } - ]; - options.push({ - label: i18next.t("menuUiHandler:importSession"), - handler: () => { - this.confirmSlot(i18next.t("menuUiHandler:importSlotSelect"), () => true, slotId => this.scene.gameData.importData(GameDataType.SESSION, slotId)); - return true; - }, - keepOpen: true - }) - options.push({ - label: i18next.t("menu:cancel"), - handler: () => { - this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); - super.end(); - return true; - } - }); - this.scene.ui.showText(i18next.t("menu:selectGameMode"), null, () => this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, { options: options })); - } - return true; - } - }, { - label: "Manage Logs", - handler: () => { - this.scene.biomeChangeMode = false - //return this.logRenameMenu() - this.scene.ui.setOverlayMode(Mode.LOG_HANDLER, - (k: string) => { - if (k === undefined) { - return this.showOptions(); - } - console.log(k) - this.showOptions(); - }, () => { - this.showOptions(); - }); - return true; - } - }, { - label: "Manage Logs (Old Menu)", - handler: () => { - return this.logRenameMenu() - } - }) - options.push({ - label: i18next.t("menu:loadGame"), - handler: () => { - this.scene.biomeChangeMode = false - this.scene.ui.setOverlayMode(Mode.SAVE_SLOT, SaveSlotUiMode.LOAD, - (slotId: integer, autoSlot: integer) => { - if (slotId === -1) { - return this.showOptions(); - } - this.loadSaveSlot(slotId, autoSlot); - }); - return true; - } - }) - if (false) { - options.push({ - label: i18next.t("menu:dailyRun"), - handler: () => { - this.scene.biomeChangeMode = false - this.setupDaily(); - return true; - }, - keepOpen: true - }) - } - options.push({ - label: i18next.t("menu:settings"), - handler: () => { - this.scene.biomeChangeMode = false - this.scene.ui.setOverlayMode(Mode.SETTINGS); - return true; - }, - keepOpen: true - }); - const config: OptionSelectConfig = { - options: options, - noCancel: true, - yOffset: 47 - }; - this.scene.ui.setMode(Mode.TITLE, config); - } - - loadSaveSlot(slotId: integer, autoSlot?: integer): void { - this.scene.sessionSlotId = slotId > -1 || !loggedInUser ? slotId : loggedInUser.lastSessionSlot; - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.resetModeChain(); - this.scene.gameData.loadSession(this.scene, slotId, slotId === -1 ? this.lastSessionData : undefined, autoSlot).then((success: boolean) => { - if (success) { - this.loaded = true; - this.scene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); - } else { - this.end(); - } - }).catch(err => { - console.error(err); - this.scene.ui.showText(i18next.t("menu:failedToLoadSession"), null); - }); - } - - initDailyRun(): void { - this.scene.ui.setMode(Mode.SAVE_SLOT, SaveSlotUiMode.SAVE, (slotId: integer) => { - this.scene.clearPhaseQueue(); - if (slotId === -1) { - this.scene.pushPhase(new TitlePhase(this.scene)); - return super.end(); - } - this.scene.sessionSlotId = slotId; - - const generateDaily = (seed: string) => { - this.scene.gameMode = getGameMode(GameModes.DAILY); - - this.scene.setSeed(seed); - this.scene.resetSeed(1); - - this.scene.money = this.scene.gameMode.getStartingMoney(); - - const starters = getDailyRunStarters(this.scene, seed); - const startingLevel = this.scene.gameMode.getStartingLevel(); - - const party = this.scene.getParty(); - const loadPokemonAssets: Promise[] = []; - for (const starter of starters) { - const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); - const starterGender = starter.species.malePercent !== null - ? !starterProps.female ? Gender.MALE : Gender.FEMALE - : Gender.GENDERLESS; - const starterPokemon = this.scene.addPlayerPokemon(starter.species, startingLevel, starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, undefined, starter.nature); - starterPokemon.setVisible(false); - party.push(starterPokemon); - loadPokemonAssets.push(starterPokemon.loadAssets()); - } - - regenerateModifierPoolThresholds(party, ModifierPoolType.DAILY_STARTER); - const modifiers: Modifier[] = Array(3).fill(null).map(() => modifierTypes.EXP_SHARE().withIdFromFunc(modifierTypes.EXP_SHARE).newModifier()) - .concat(Array(3).fill(null).map(() => modifierTypes.GOLDEN_EXP_CHARM().withIdFromFunc(modifierTypes.GOLDEN_EXP_CHARM).newModifier())) - .concat(getDailyRunStarterModifiers(party)) - .filter((m) => m !== null); - - for (const m of modifiers) { - this.scene.addModifier(m, true, false, false, true); - } - this.scene.updateModifiers(true, true); - - Promise.all(loadPokemonAssets).then(() => { - this.scene.time.delayedCall(500, () => this.scene.playBgm()); - this.scene.gameData.gameStats.dailyRunSessionsPlayed++; - this.scene.newArena(this.scene.gameMode.getStartingBiome(this.scene)); - this.scene.newBattle(); - this.scene.arena.init(); - this.scene.sessionPlayTime = 0; - this.scene.lastSavePlayTime = 0; - this.end(); - }); - }; - - // If Online, calls seed fetch from db to generate daily run. If Offline, generates a daily run based on current date. - if (!Utils.isLocal) { - fetchDailyRunSeed().then(seed => { - if (seed) { - generateDaily(seed); - } else { - throw new Error("Daily run seed is null!"); - } - }).catch(err => { - console.error("Failed to load daily run:\n", err); - }); - } else { - generateDaily(btoa(new Date().toISOString().substring(0, 10))); - } - }); - } - setupDaily(): void { - // TODO - var saves = this.getSaves() - var saveNames = new Array(5).fill("") - for (var i = 0; i < saves!.length; i++) { - saveNames[saves![i][0]] = saves![i][1].description - } - const ui = this.scene.ui - const confirmSlot = (message: string, slotFilter: (i: integer) => boolean, callback: (i: integer) => void) => { - ui.revertMode(); - ui.showText(message, null, () => { - const config: OptionSelectConfig = { - options: new Array(5).fill(null).map((_, i) => i).filter(slotFilter).map(i => { - return { - label: (i+1) + " " + saveNames[i], - handler: () => { - callback(i); - ui.revertMode(); - ui.showText("", 0); - return true; - } - }; - }).concat([{ - label: i18next.t("menuUiHandler:cancel"), - handler: () => { - ui.revertMode(); - ui.showText("", 0); - return true; - } - }]), - xOffset: 98 - }; - ui.setOverlayMode(Mode.MENU_OPTION_SELECT, config); - }); - }; - ui.showText("This feature is incomplete.", null, () => { - this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); - super.end(); - return true; - }) - return; - confirmSlot("Select a slot to replace.", () => true, slotId => this.scene.gameData.importData(GameDataType.SESSION, slotId)); - } - end(): void { - this.scene.biomeChangeMode = false - if (!this.loaded && !this.scene.gameMode.isDaily) { - this.scene.arena.preloadBgm(); - this.scene.gameMode = getGameMode(this.gameMode); - if (this.gameMode === GameModes.CHALLENGE) { - this.scene.pushPhase(new SelectChallengePhase(this.scene)); - } else { - this.scene.pushPhase(new SelectStarterPhase(this.scene)); - } - this.scene.newArena(this.scene.gameMode.getStartingBiome(this.scene)); - } else { - this.scene.playBgm(); - } - - this.scene.pushPhase(new EncounterPhase(this.scene, this.loaded)); - - if (this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; - - this.scene.pushPhase(new SummonPhase(this.scene, 0, true, true)); - if (this.scene.currentBattle.double && availablePartyMembers > 1) { - this.scene.pushPhase(new SummonPhase(this.scene, 1, true, true)); - } - - if (this.scene.currentBattle.battleType !== BattleType.TRAINER && (this.scene.currentBattle.waveIndex > 1 || !this.scene.gameMode.isDaily)) { - const minPartySize = this.scene.currentBattle.double ? 2 : 1; - if (availablePartyMembers > minPartySize) { - this.scene.pushPhase(new CheckSwitchPhase(this.scene, 0, this.scene.currentBattle.double)); - if (this.scene.currentBattle.double) { - this.scene.pushPhase(new CheckSwitchPhase(this.scene, 1, this.scene.currentBattle.double)); - } - } - } - } - - for (const achv of Object.keys(this.scene.gameData.achvUnlocks)) { - if (vouchers.hasOwnProperty(achv)) { - this.scene.validateVoucher(vouchers[achv]); - } - } - - super.end(); - } -} -//#endregion - - - - - -//#region 04 UnavailablePhase -export class UnavailablePhase extends Phase { - constructor(scene: BattleScene) { - super(scene); - } - - start(): void { - this.scene.ui.setMode(Mode.UNAVAILABLE, () => { - this.scene.unshiftPhase(new LoginPhase(this.scene, true)); - this.end(); - }); - } -} -//#endregion - - - - - -//#region 05 ReloadSessionPhase -export class ReloadSessionPhase extends Phase { - private systemDataStr: string | null; - - constructor(scene: BattleScene, systemDataStr?: string) { - super(scene); - - this.systemDataStr = systemDataStr ?? null; - } - - start(): void { - this.scene.ui.setMode(Mode.SESSION_RELOAD); - - let delayElapsed = false; - let loaded = false; - - this.scene.time.delayedCall(Utils.fixedInt(1500), () => { - if (loaded) { - this.end(); - } else { - delayElapsed = true; - } - }); - - this.scene.gameData.clearLocalData(); - - (this.systemDataStr ? this.scene.gameData.initSystem(this.systemDataStr) : this.scene.gameData.loadSystem()).then(() => { - if (delayElapsed) { - this.end(); - } else { - loaded = true; - } - }); - } -} -//#endregion - - - - - -//#region 06 OutdatedPhase -export class OutdatedPhase extends Phase { - constructor(scene: BattleScene) { - super(scene); - } - - start(): void { - this.scene.ui.setMode(Mode.OUTDATED); - } -} -//#endregion - - - - - -//#region 07 SelectGenderPhase -export class SelectGenderPhase extends Phase { - constructor(scene: BattleScene) { - super(scene); - } - - start(): void { - super.start(); - - this.scene.ui.showText(i18next.t("menu:boyOrGirl"), null, () => { - this.scene.ui.setMode(Mode.OPTION_SELECT, { - options: [ - { - label: i18next.t("settings:boy"), - handler: () => { - this.scene.gameData.gender = PlayerGender.MALE; - this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 0); - this.scene.gameData.saveSystem().then(() => this.end()); - return true; - } - }, - { - label: i18next.t("settings:girl"), - handler: () => { - this.scene.gameData.gender = PlayerGender.FEMALE; - this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 1); - this.scene.gameData.saveSystem().then(() => this.end()); - return true; - } - } - ] - }); - }); - } - - end(): void { - this.scene.ui.setMode(Mode.MESSAGE); - super.end(); - } -} -//#endregion - - - - - -//#region 08 SelectChallengePhase -export class SelectChallengePhase extends Phase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - this.scene.playBgm("menu"); - - this.scene.ui.setMode(Mode.CHALLENGE_SELECT); - } -} -//#endregion - - - - - -//#region 09 SelectStarterPhase -export class SelectStarterPhase extends Phase { - - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - this.scene.playBgm("menu"); - - this.scene.ui.setMode(Mode.STARTER_SELECT, (starters: Starter[]) => { - this.scene.ui.clearText(); - this.scene.ui.setMode(Mode.SAVE_SLOT, SaveSlotUiMode.SAVE, (slotId: integer) => { - if (slotId === -1) { - this.scene.clearPhaseQueue(); - this.scene.pushPhase(new TitlePhase(this.scene)); - return this.end(); - } - this.scene.sessionSlotId = slotId; - this.initBattle(starters); - }); - }); - } - - /** - * Initialize starters before starting the first battle - * @param starters {@linkcode Pokemon} with which to start the first battle - */ - initBattle(starters: Starter[]) { - const party = this.scene.getParty(); - const loadPokemonAssets: Promise[] = []; - starters.forEach((starter: Starter, i: integer) => { - if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { - starter.species = getPokemonSpecies(Overrides.STARTER_SPECIES_OVERRIDE as Species); - } - const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); - if ( - starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES && - starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!] - ) { - starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!; - } - - let starterGender = starter.species.malePercent !== null - ? !starterProps.female ? Gender.MALE : Gender.FEMALE - : Gender.GENDERLESS; - if (Overrides.GENDER_OVERRIDE !== null) { - starterGender = Overrides.GENDER_OVERRIDE; - } - const starterIvs = this.scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); - const starterPokemon = this.scene.addPlayerPokemon(starter.species, this.scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature); - starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); - if (starter.passive) { - starterPokemon.passive = true; - } - starterPokemon.luck = this.scene.gameData.getDexAttrLuck(this.scene.gameData.dexData[starter.species.speciesId].caughtAttr); - if (starter.pokerus) { - starterPokemon.pokerus = true; - } - - if (starter.nickname) { - starterPokemon.nickname = starter.nickname; - } - - if (this.scene.gameMode.isSplicedOnly) { - starterPokemon.generateFusionSpecies(true); - } - starterPokemon.setVisible(false); - applyChallenges(this.scene.gameMode, ChallengeType.STARTER_MODIFY, starterPokemon); - party.push(starterPokemon); - loadPokemonAssets.push(starterPokemon.loadAssets()); - }); - overrideModifiers(this.scene); - overrideHeldItems(this.scene, party[0]); - Promise.all(loadPokemonAssets).then(() => { - SoundFade.fadeOut(this.scene, this.scene.sound.get("menu"), 500, true); - this.scene.time.delayedCall(500, () => this.scene.playBgm()); - if (this.scene.gameMode.isClassic) { - this.scene.gameData.gameStats.classicSessionsPlayed++; - } else { - this.scene.gameData.gameStats.endlessSessionsPlayed++; - } - this.scene.newBattle(); - this.scene.arena.init(); - this.scene.sessionPlayTime = 0; - this.scene.lastSavePlayTime = 0; - // Ensures Keldeo (or any future Pokemon that have this type of form change) starts in the correct form - this.scene.getParty().forEach((p: PlayerPokemon) => { - this.scene.triggerPokemonFormChange(p, SpeciesFormChangeMoveLearnedTrigger); - }); - this.end(); - }); - } -} -//#endregion - - - - - -//#region 10 BattlePhase -export class BattlePhase extends Phase { - constructor(scene: BattleScene) { - super(scene); - } - - showEnemyTrainer(trainerSlot: TrainerSlot = TrainerSlot.NONE): void { - const sprites = this.scene.currentBattle.trainer?.getSprites()!; // TODO: is this bang correct? - const tintSprites = this.scene.currentBattle.trainer?.getTintSprites()!; // TODO: is this bang correct? - for (let i = 0; i < sprites.length; i++) { - const visible = !trainerSlot || !i === (trainerSlot === TrainerSlot.TRAINER) || sprites.length < 2; - [sprites[i], tintSprites[i]].map(sprite => { - if (visible) { - sprite.x = trainerSlot || sprites.length < 2 ? 0 : i ? 16 : -16; - } - sprite.setVisible(visible); - sprite.clearTint(); - }); - sprites[i].setVisible(visible); - tintSprites[i].setVisible(visible); - sprites[i].clearTint(); - tintSprites[i].clearTint(); - } - this.scene.tweens.add({ - targets: this.scene.currentBattle.trainer, - x: "-=16", - y: "+=16", - alpha: 1, - ease: "Sine.easeInOut", - duration: 750 - }); - } - - hideEnemyTrainer(): void { - this.scene.tweens.add({ - targets: this.scene.currentBattle.trainer, - x: "+=16", - y: "-=16", - alpha: 0, - ease: "Sine.easeInOut", - duration: 750 - }); - } -} -//#endregion - - - - - -//#region 11 FieldPhase -type PokemonFunc = (pokemon: Pokemon) => void; -export abstract class FieldPhase extends BattlePhase { - getOrder(): BattlerIndex[] { - const playerField = this.scene.getPlayerField().filter(p => p.isActive()) as Pokemon[]; - const enemyField = this.scene.getEnemyField().filter(p => p.isActive()) as Pokemon[]; - - // We shuffle the list before sorting so speed ties produce random results - let orderedTargets: Pokemon[] = playerField.concat(enemyField); - // We seed it with the current turn to prevent an inconsistency where it - // was varying based on how long since you last reloaded - this.scene.executeWithSeedOffset(() => { - orderedTargets = Utils.randSeedShuffle(orderedTargets); - }, this.scene.currentBattle.turn, this.scene.waveSeed); - - orderedTargets.sort((a: Pokemon, b: Pokemon) => { - const aSpeed = a?.getBattleStat(Stat.SPD) || 0; - const bSpeed = b?.getBattleStat(Stat.SPD) || 0; - - return bSpeed - aSpeed; - }); - - const speedReversed = new Utils.BooleanHolder(false); - this.scene.arena.applyTags(TrickRoomTag, speedReversed); - - if (speedReversed.value) { - orderedTargets = orderedTargets.reverse(); - } - - return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : 0)); - } - - executeForAll(func: PokemonFunc): void { - const field = this.scene.getField(true).filter(p => p.summonData); - field.forEach(pokemon => func(pokemon)); - } -} -//#endregion - - - - - -//#region 12 PokemonPhase -export abstract class PokemonPhase extends FieldPhase { - protected battlerIndex: BattlerIndex | integer; - public player: boolean; - public fieldIndex: integer; - - constructor(scene: BattleScene, battlerIndex?: BattlerIndex | integer) { - super(scene); - - if (battlerIndex === undefined) { - battlerIndex = scene.getField().find(p => p?.isActive())!.getBattlerIndex(); // TODO: is the bang correct here? - } - - this.battlerIndex = battlerIndex; - this.player = battlerIndex < 2; - this.fieldIndex = battlerIndex % 2; - } - - getPokemon(): Pokemon { - if (this.battlerIndex > BattlerIndex.ENEMY_2) { - return this.scene.getPokemonById(this.battlerIndex)!; //TODO: is this bang correct? - } - return this.scene.getField()[this.battlerIndex]!; //TODO: is this bang correct? - } -} -//#endregion - - - - - -//#region 13 PartyMembPkmn -export abstract class PartyMemberPokemonPhase extends FieldPhase { - protected partyMemberIndex: integer; - protected fieldIndex: integer; - protected player: boolean; - - constructor(scene: BattleScene, partyMemberIndex: integer, player: boolean) { - super(scene); - - this.partyMemberIndex = partyMemberIndex; - this.fieldIndex = partyMemberIndex < this.scene.currentBattle.getBattlerCount() - ? partyMemberIndex - : -1; - this.player = player; - } - - getParty(): Pokemon[] { - return this.player ? this.scene.getParty() : this.scene.getEnemyParty(); - } - - getPokemon(): Pokemon { - return this.getParty()[this.partyMemberIndex]; - } -} -//#endregion - - - - - -//#region 14 PlayerPartyMemberPokemonPhase -export abstract class PlayerPartyMemberPokemonPhase extends PartyMemberPokemonPhase { - constructor(scene: BattleScene, partyMemberIndex: integer) { - super(scene, partyMemberIndex, true); - } - - getPlayerPokemon(): PlayerPokemon { - return super.getPokemon() as PlayerPokemon; - } -} -//#endregion - - - - - -//#region 15 EnemyPartyMemberPokemonPhase -export abstract class EnemyPartyMemberPokemonPhase extends PartyMemberPokemonPhase { - constructor(scene: BattleScene, partyMemberIndex: integer) { - super(scene, partyMemberIndex, false); - } - - getEnemyPokemon(): EnemyPokemon { - return super.getPokemon() as EnemyPokemon; - } -} -//#endregion - - - - - -//#region 16 EncounterPhase -export class EncounterPhase extends BattlePhase { - private loaded: boolean; - - constructor(scene: BattleScene, loaded?: boolean) { - super(scene); - - this.loaded = !!loaded; - } - - start() { - super.start(); - - this.scene.updateGameInfo(); - - this.scene.initSession(); - - this.scene.eventTarget.dispatchEvent(new EncounterPhaseEvent()); - - // Failsafe if players somehow skip floor 200 in classic mode - if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex > 200) { - this.scene.unshiftPhase(new GameOverPhase(this.scene)); - } - - const loadEnemyAssets: Promise[] = []; - - const battle = this.scene.currentBattle; - - let totalBst = 0; - - while (LoggerTools.rarities.length > 0) { - LoggerTools.rarities.pop() - } - LoggerTools.rarityslot[0] = 0 - //console.log(this.scene.gameMode.getDailyOverride()) - battle.enemyLevels?.forEach((level, e) => { - if (!this.loaded) { - if (battle.battleType === BattleType.TRAINER) { - battle.enemyParty[e] = battle.trainer?.genPartyMember(e)!; // TODO:: is the bang correct here? - } else { - LoggerTools.rarityslot[0] = e - const enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true); - battle.enemyParty[e] = this.scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, !!this.scene.getEncounterBossSegments(battle.waveIndex, level, enemySpecies)); - if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { - battle.enemyParty[e].ivs = new Array(6).fill(31); - } - this.scene.getParty().slice(0, !battle.double ? 1 : 2).reverse().forEach(playerPokemon => { - applyAbAttrs(SyncEncounterNatureAbAttr, playerPokemon, null, battle.enemyParty[e]); - }); - } - } - const enemyPokemon = this.scene.getEnemyParty()[e]; - if (e < (battle.double ? 2 : 1)) { - enemyPokemon.setX(-66 + enemyPokemon.getFieldPositionOffset()[0]); - enemyPokemon.resetSummonData(); - } - - if (!this.loaded) { - this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER); - } - - if (enemyPokemon.species.speciesId === Species.ETERNATUS) { - if (this.scene.gameMode.isClassic && (battle.battleSpec === BattleSpec.FINAL_BOSS || this.scene.gameMode.isWaveFinal(battle.waveIndex))) { - if (battle.battleSpec !== BattleSpec.FINAL_BOSS) { - enemyPokemon.formIndex = 1; - enemyPokemon.updateScale(); - } - enemyPokemon.setBoss(); - } else if (!(battle.waveIndex % 1000)) { - enemyPokemon.formIndex = 1; - enemyPokemon.updateScale(); - const bossMBH = this.scene.findModifier(m => m instanceof TurnHeldItemTransferModifier && m.pokemonId === enemyPokemon.id, false) as TurnHeldItemTransferModifier; - this.scene.removeModifier(bossMBH!); - bossMBH?.setTransferrableFalse(); - this.scene.addEnemyModifier(bossMBH!); - } - } - - totalBst += enemyPokemon.getSpeciesForm().baseTotal; - - loadEnemyAssets.push(enemyPokemon.loadAssets()); - - console.log(getPokemonNameWithAffix(enemyPokemon), enemyPokemon.species.speciesId, enemyPokemon.stats); - }); - console.log(LoggerTools.rarities) - - if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { - this.scene.validateAchv(achvs.SHINY_PARTY); - } - - if (battle.battleType === BattleType.TRAINER) { - loadEnemyAssets.push(battle.trainer?.loadAssets().then(() => battle.trainer?.initSprite())!); // TODO: is this bang correct? - } else { - // This block only applies for double battles to init the boss segments (idk why it's split up like this) - if (battle.enemyParty.filter(p => p.isBoss()).length > 1) { - for (const enemyPokemon of battle.enemyParty) { - // If the enemy pokemon is a boss and wasn't populated from data source, then set it up - if (enemyPokemon.isBoss() && !enemyPokemon.isPopulatedFromDataSource) { - enemyPokemon.setBoss(true, Math.ceil(enemyPokemon.bossSegments * (enemyPokemon.getSpeciesForm().baseTotal / totalBst))); - enemyPokemon.initBattleInfo(); - } - } - } - } - - Promise.all(loadEnemyAssets).then(() => { - battle.enemyParty.forEach((enemyPokemon, e) => { - if (e < (battle.double ? 2 : 1)) { - if (battle.battleType === BattleType.WILD) { - this.scene.field.add(enemyPokemon); - battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); - const playerPokemon = this.scene.getPlayerPokemon(); - if (playerPokemon?.visible) { - this.scene.field.moveBelow(enemyPokemon as Pokemon, playerPokemon); - } - enemyPokemon.tint(0, 0.5); - } else if (battle.battleType === BattleType.TRAINER) { - enemyPokemon.setVisible(false); - this.scene.currentBattle.trainer?.tint(0, 0.5); - } - if (battle.double) { - enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT); - } - } - }); - - if (!this.loaded) { - regenerateModifierPoolThresholds(this.scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD); - this.scene.generateEnemyModifiers(); - } - - this.scene.ui.setMode(Mode.MESSAGE).then(() => { - if (!this.loaded) { - //@ts-ignore - this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || this.scene.lastSavePlayTime >= 300).then(success => { // TODO: get rid of ts-ignore - this.scene.disableMenu = false; - if (!success) { - return this.scene.reset(true); - } - this.doEncounter(); - }); - } else { - this.doEncounter(); - } - }); - }); - } - - doEncounter() { - this.scene.playBgm(undefined, true); - this.scene.updateModifiers(false); - this.scene.setFieldScale(1); - - /*if (startingWave > 10) { - for (let m = 0; m < Math.min(Math.floor(startingWave / 10), 99); m++) - this.scene.addModifier(getPlayerModifierTypeOptionsForWave((m + 1) * 10, 1, this.scene.getParty())[0].type.newModifier(), true); - this.scene.updateModifiers(true); - }*/ - - for (const pokemon of this.scene.getParty()) { - if (pokemon) { - pokemon.resetBattleData(); - } - } - - if (!this.loaded) { - this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); - } - - const enemyField = this.scene.getEnemyField(); - this.scene.tweens.add({ - targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(), - x: (_target, _key, value, fieldIndex: integer) => fieldIndex < 2 + (enemyField.length) ? value + 300 : value - 300, - duration: 2000, - onComplete: () => { - if (!this.tryOverrideForBattleSpec()) { - this.doEncounterCommon(); - } - } - }); - } - - getEncounterMessage(): string { - const enemyField = this.scene.getEnemyField(); - - if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { - return i18next.t("battle:bossAppeared", { bossName: getPokemonNameWithAffix(enemyField[0])}); - } - - if (this.scene.currentBattle.battleType === BattleType.TRAINER) { - if (this.scene.currentBattle.double) { - return i18next.t("battle:trainerAppearedDouble", { trainerName: this.scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }); - - } else { - return i18next.t("battle:trainerAppeared", { trainerName: this.scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }); - } - } - - return enemyField.length === 1 - ? i18next.t("battle:singleWildAppeared", { pokemonName: enemyField[0].getNameToRender() }) - : i18next.t("battle:multiWildAppeared", { pokemonName1: enemyField[0].getNameToRender(), pokemonName2: enemyField[1].getNameToRender() }); - } - - doEncounterCommon(showEncounterMessage: boolean = true) { - const enemyField = this.scene.getEnemyField(); - - //LoggerTools.resetWave(this.scene, this.scene.currentBattle.waveIndex) - if (this.scene.lazyReloads) { - LoggerTools.flagResetIfExists(this.scene) - } - LoggerTools.logTeam(this.scene, this.scene.currentBattle.waveIndex) - if (this.scene.getEnemyParty()[0].hasTrainer()) { - LoggerTools.logTrainer(this.scene, this.scene.currentBattle.waveIndex) - } - if (this.scene.currentBattle.waveIndex == 1) { - LoggerTools.logPlayerTeam(this.scene) - if (this.scene.gameMode.modeId == GameModes.DAILY && this.scene.disableDailyShinies) { - this.scene.getParty().forEach(p => { - p.species.luckOverride = 0; // Disable shiny luck for party members - }) - } - } - LoggerTools.resetWaveActions(this.scene, undefined, true) - - //this.scene.doShinyCheck() - - if (LoggerTools.autoCheckpoints.includes(this.scene.currentBattle.waveIndex)) { - //this.scene.gameData.saveGameToAuto(this.scene) - } - - if (this.scene.currentBattle.battleType === BattleType.WILD) { - enemyField.forEach(enemyPokemon => { - enemyPokemon.untint(100, "Sine.easeOut"); - enemyPokemon.cry(); - enemyPokemon.showInfo(); - if (enemyPokemon.isShiny()) { - this.scene.validateAchv(achvs.SEE_SHINY); - } - }); - this.scene.updateFieldScale(); - if (showEncounterMessage) { - this.scene.ui.showText(this.getEncounterMessage(), null, () => this.end(), 1500); - } else { - this.end(); - } - } else if (this.scene.currentBattle.battleType === BattleType.TRAINER) { - const trainer = this.scene.currentBattle.trainer; - trainer?.untint(100, "Sine.easeOut"); - trainer?.playAnim(); - - const doSummon = () => { - this.scene.currentBattle.started = true; - this.scene.playBgm(undefined); - this.scene.pbTray.showPbTray(this.scene.getParty()); - this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); - const doTrainerSummon = () => { - this.hideEnemyTrainer(); - const availablePartyMembers = this.scene.getEnemyParty().filter(p => !p.isFainted()).length; - this.scene.unshiftPhase(new SummonPhase(this.scene, 0, false)); - if (this.scene.currentBattle.double && availablePartyMembers > 1) { - this.scene.unshiftPhase(new SummonPhase(this.scene, 1, false)); - } - this.end(); - }; - if (showEncounterMessage) { - this.scene.ui.showText(this.getEncounterMessage(), null, doTrainerSummon, 1500, true); - } else { - doTrainerSummon(); - } - }; - - const encounterMessages = this.scene.currentBattle.trainer?.getEncounterMessages(); - - if (!encounterMessages?.length) { - doSummon(); - } else { - let message: string; - this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex); - message = message!; // tell TS compiler it's defined now - const showDialogueAndSummon = () => { - this.scene.ui.showDialogue(message, trainer?.getName(TrainerSlot.NONE, true), null, () => { - this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doSummon())); - }); - }; - if (this.scene.currentBattle.trainer?.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) { - this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(trainer?.getKey()!, getCharVariantFromDialogue(encounterMessages[0])).then(() => showDialogueAndSummon())); // TODO: is this bang correct? - } else { - showDialogueAndSummon(); - } - } - } - } - - end() { - const enemyField = this.scene.getEnemyField(); - - enemyField.forEach((enemyPokemon, e) => { - if (enemyPokemon.isShiny()) { - this.scene.unshiftPhase(new ShinySparklePhase(this.scene, BattlerIndex.ENEMY + e)); - } - }); - - if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { - enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => { - // if there is not a player party, we can't continue - if (!this.scene.getParty()?.length) { - return false; - } - // how many player pokemon are on the field ? - const pokemonsOnFieldCount = this.scene.getParty().filter(p => p.isOnField()).length; - // if it's a 2vs1, there will never be a 2nd pokemon on our field even - const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2); - // if it's a double, there should be 2, otherwise 1 - if (this.scene.currentBattle.double) { - return pokemonsOnFieldCount === requiredPokemonsOnField; - } - return pokemonsOnFieldCount === 1; - })); - const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier); - if (ivScannerModifier) { - enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)))); - } - } - - if (!this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); - - if (!availablePartyMembers[0].isOnField()) { - this.scene.pushPhase(new SummonPhase(this.scene, 0)); - } - - if (this.scene.currentBattle.double) { - if (availablePartyMembers.length > 1) { - this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, true)); - if (!availablePartyMembers[1].isOnField()) { - this.scene.pushPhase(new SummonPhase(this.scene, 1)); - } - } - } else { - if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { - this.scene.pushPhase(new ReturnPhase(this.scene, 1)); - } - this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, false)); - } - - if (this.scene.currentBattle.battleType !== BattleType.TRAINER && (this.scene.currentBattle.waveIndex > 1 || !this.scene.gameMode.isDaily)) { - const minPartySize = this.scene.currentBattle.double ? 2 : 1; - if (availablePartyMembers.length > minPartySize) { - this.scene.pushPhase(new CheckSwitchPhase(this.scene, 0, this.scene.currentBattle.double)); - if (this.scene.currentBattle.double) { - this.scene.pushPhase(new CheckSwitchPhase(this.scene, 1, this.scene.currentBattle.double)); - } - } - } - } - handleTutorial(this.scene, Tutorial.Access_Menu).then(() => { - // Auto-show the flyout - if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { - this.scene.arenaFlyout.display2() - this.scene.arenaFlyout.toggleFlyout(true) - this.scene.arenaFlyout.isAuto = true - } - super.end() - }); - } - - tryOverrideForBattleSpec(): boolean { - switch (this.scene.currentBattle.battleSpec) { - case BattleSpec.FINAL_BOSS: - const enemy = this.scene.getEnemyPokemon(); - this.scene.ui.showText(this.getEncounterMessage(), null, () => { - const count = 5643853 + this.scene.gameData.gameStats.classicSessionsPlayed; - //The two lines below check if English ordinals (1st, 2nd, 3rd, Xth) are used and determine which one to use. - //Otherwise, it defaults to an empty string. - //As of 08-07-24: Spanish and Italian default to the English translations - const ordinalUse = ["en", "es", "it"]; - const currentLanguage = i18next.resolvedLanguage ?? "en"; - const ordinalIndex = (ordinalUse.includes(currentLanguage)) ? ["st", "nd", "rd"][((count + 90) % 100 - 10) % 10 - 1] ?? "th" : ""; - const cycleCount = count.toLocaleString() + ordinalIndex; - const encounterDialogue = i18next.t(`${(this.scene.gameData.gender === PlayerGender.FEMALE) ? "PGF" : "PGM"}battleSpecDialogue:encounter`, {cycleCount: cycleCount}); - this.scene.ui.showDialogue(encounterDialogue, enemy?.species.name, null, () => { - this.doEncounterCommon(false); - }); - }, 1500, true); - return true; - } - - return false; - } -} -//#endregion - - - - - -//#region 17 NextEncounterPhase -export class NextEncounterPhase extends EncounterPhase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - } - - doEncounter(): void { - this.scene.playBgm(undefined, true); - - for (const pokemon of this.scene.getParty()) { - if (pokemon) { - pokemon.resetBattleData(); - } - } - - this.scene.arenaNextEnemy.setBiome(this.scene.arena.biomeType); - this.scene.arenaNextEnemy.setVisible(true); - - const enemyField = this.scene.getEnemyField(); - this.scene.tweens.add({ - targets: [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer].flat(), - x: "+=300", - duration: 2000, - onComplete: () => { - this.scene.arenaEnemy.setBiome(this.scene.arena.biomeType); - this.scene.arenaEnemy.setX(this.scene.arenaNextEnemy.x); - this.scene.arenaEnemy.setAlpha(1); - this.scene.arenaNextEnemy.setX(this.scene.arenaNextEnemy.x - 300); - this.scene.arenaNextEnemy.setVisible(false); - if (this.scene.lastEnemyTrainer) { - this.scene.lastEnemyTrainer.destroy(); - } - - if (!this.tryOverrideForBattleSpec()) { - this.doEncounterCommon(); - } - } - }); - } -} -//#endregion - - - - - -//#region 18 NewBiomeEncounterPhase -export class NewBiomeEncounterPhase extends NextEncounterPhase { - constructor(scene: BattleScene) { - super(scene); - } - - doEncounter(): void { - this.scene.playBgm(undefined, true); - - for (const pokemon of this.scene.getParty()) { - if (pokemon) { - pokemon.resetBattleData(); - } - } - - this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); - - for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) { - applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); - } - - const enemyField = this.scene.getEnemyField(); - this.scene.tweens.add({ - targets: [this.scene.arenaEnemy, enemyField].flat(), - x: "+=300", - duration: 2000, - onComplete: () => { - if (!this.tryOverrideForBattleSpec()) { - this.doEncounterCommon(); - } - } - }); - } -} -//#endregion - - - - - -//#region 19 PostSummonPhase -export class PostSummonPhase extends PokemonPhase { - constructor(scene: BattleScene, battlerIndex: BattlerIndex) { - super(scene, battlerIndex); - } - - start() { - super.start(); - - const pokemon = this.getPokemon(); - - if (pokemon.status?.effect === StatusEffect.TOXIC) { - pokemon.status.turnCount = 0; - } - this.scene.arena.applyTags(ArenaTrapTag, pokemon); - applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end()); - } -} -//#endregion - - - - - -//#region 20 SelectBiomePh. -export class SelectBiomePhase extends BattlePhase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - const currentBiome = this.scene.arena.biomeType; - - const setNextBiome = (nextBiome: Biome) => { - if (this.scene.currentBattle.waveIndex % 10 === 1) { - this.scene.applyModifiers(MoneyInterestModifier, true, this.scene); - this.scene.unshiftPhase(new PartyHealPhase(this.scene, false)); - } - this.scene.unshiftPhase(new SwitchBiomePhase(this.scene, nextBiome)); - this.end(); - }; - - if ((this.scene.gameMode.isClassic && this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex + 9)) - || (this.scene.gameMode.isDaily && this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) - || (this.scene.gameMode.hasShortBiomes && !(this.scene.currentBattle.waveIndex % 50))) { - setNextBiome(Biome.END); - } else if (this.scene.gameMode.hasRandomBiomes) { - setNextBiome(this.generateNextBiome()); - } else if (Array.isArray(biomeLinks[currentBiome])) { - let biomes: Biome[] = []; - this.scene.executeWithSeedOffset(() => { - biomes = (biomeLinks[currentBiome] as (Biome | [Biome, integer])[]) - .filter(b => !Array.isArray(b) || !Utils.randSeedInt(b[1])) - .map(b => !Array.isArray(b) ? b : b[0]); - }, this.scene.currentBattle.waveIndex); - if (biomes.length > 1 && this.scene.findModifier(m => m instanceof MapModifier)) { - let biomeChoices: Biome[] = []; - this.scene.executeWithSeedOffset(() => { - biomeChoices = (!Array.isArray(biomeLinks[currentBiome]) - ? [biomeLinks[currentBiome] as Biome] - : biomeLinks[currentBiome] as (Biome | [Biome, integer])[]) - .filter((b, i) => !Array.isArray(b) || !Utils.randSeedInt(b[1])) - .map(b => Array.isArray(b) ? b[0] : b); - }, this.scene.currentBattle.waveIndex); - const biomeSelectItems = biomeChoices.map(b => { - const ret: OptionSelectItem = { - label: getBiomeName(b), - handler: () => { - this.scene.ui.setMode(Mode.MESSAGE); - setNextBiome(b); - return true; - } - }; - return ret; - }); - this.scene.ui.setMode(Mode.OPTION_SELECT, { - options: biomeSelectItems, - delay: 1000 - }); - } else { - setNextBiome(biomes[Utils.randSeedInt(biomes.length)]); - } - } else if (biomeLinks.hasOwnProperty(currentBiome)) { - setNextBiome(biomeLinks[currentBiome] as Biome); - } else { - setNextBiome(this.generateNextBiome()); - } - } - - generateNextBiome(): Biome { - if (!(this.scene.currentBattle.waveIndex % 50)) { - return Biome.END; - } - return this.scene.generateRandomBiome(this.scene.currentBattle.waveIndex); - } -} -//#endregion - - - - - -//#region 21 SwitchBiomePhase -export class SwitchBiomePhase extends BattlePhase { - private nextBiome: Biome; - - constructor(scene: BattleScene, nextBiome: Biome) { - super(scene); - - this.nextBiome = nextBiome; - } - - start() { - super.start(); - - if (this.nextBiome === undefined) { - return this.end(); - } - - this.scene.tweens.add({ - targets: [this.scene.arenaEnemy, this.scene.lastEnemyTrainer], - x: "+=300", - duration: 2000, - onComplete: () => { - this.scene.arenaEnemy.setX(this.scene.arenaEnemy.x - 600); - - this.scene.newArena(this.nextBiome); - - const biomeKey = getBiomeKey(this.nextBiome); - const bgTexture = `${biomeKey}_bg`; - this.scene.arenaBgTransition.setTexture(bgTexture); - this.scene.arenaBgTransition.setAlpha(0); - this.scene.arenaBgTransition.setVisible(true); - this.scene.arenaPlayerTransition.setBiome(this.nextBiome); - this.scene.arenaPlayerTransition.setAlpha(0); - this.scene.arenaPlayerTransition.setVisible(true); - - this.scene.tweens.add({ - targets: [this.scene.arenaPlayer, this.scene.arenaBgTransition, this.scene.arenaPlayerTransition], - duration: 1000, - delay: 1000, - ease: "Sine.easeInOut", - alpha: (target: any) => target === this.scene.arenaPlayer ? 0 : 1, - onComplete: () => { - this.scene.arenaBg.setTexture(bgTexture); - this.scene.arenaPlayer.setBiome(this.nextBiome); - this.scene.arenaPlayer.setAlpha(1); - this.scene.arenaEnemy.setBiome(this.nextBiome); - this.scene.arenaEnemy.setAlpha(1); - this.scene.arenaNextEnemy.setBiome(this.nextBiome); - this.scene.arenaBgTransition.setVisible(false); - this.scene.arenaPlayerTransition.setVisible(false); - if (this.scene.lastEnemyTrainer) { - this.scene.lastEnemyTrainer.destroy(); - } - - this.end(); - } - }); - } - }); - } -} -//#endregion - - - - - -//#region 22 SummonPhase -export class SummonPhase extends PartyMemberPokemonPhase { - private loaded: boolean; - - constructor(scene: BattleScene, fieldIndex: integer, player: boolean = true, loaded: boolean = false) { - super(scene, fieldIndex, player); - - this.loaded = loaded; - } - - start() { - super.start(); - - this.preSummon(); - } - - /** - * Sends out a Pokemon before the battle begins and shows the appropriate messages - */ - preSummon(): void { - const partyMember = this.getPokemon(); - // If the Pokemon about to be sent out is fainted or illegal under a challenge, switch to the first non-fainted legal Pokemon - if (!partyMember.isAllowedInBattle()) { - console.warn("The Pokemon about to be sent out is fainted or illegal under a challenge. Attempting to resolve..."); - - // First check if they're somehow still in play, if so remove them. - if (partyMember.isOnField()) { - partyMember.leaveField(); - } - - const party = this.getParty(); - - // Find the first non-fainted Pokemon index above the current one - const legalIndex = party.findIndex((p, i) => i > this.partyMemberIndex && p.isAllowedInBattle()); - if (legalIndex === -1) { - console.error("Party Details:\n", party); - console.error("All available Pokemon were fainted or illegal!"); - this.scene.clearPhaseQueue(); - this.scene.unshiftPhase(new GameOverPhase(this.scene)); - this.end(); - return; - } - - // Swaps the fainted Pokemon and the first non-fainted legal Pokemon in the party - [party[this.partyMemberIndex], party[legalIndex]] = [party[legalIndex], party[this.partyMemberIndex]]; - console.warn("Swapped %s %O with %s %O", getPokemonNameWithAffix(partyMember), partyMember, getPokemonNameWithAffix(party[0]), party[0]); - } - - if (this.player) { - this.scene.ui.showText(i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(this.getPokemon()) })); - if (this.player) { - this.scene.pbTray.hide(); - } - this.scene.trainer.setTexture(`trainer_${this.scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); - this.scene.time.delayedCall(562, () => { - this.scene.trainer.setFrame("2"); - this.scene.time.delayedCall(64, () => { - this.scene.trainer.setFrame("3"); - }); - }); - this.scene.tweens.add({ - targets: this.scene.trainer, - x: -36, - duration: 1000, - onComplete: () => this.scene.trainer.setVisible(false) - }); - this.scene.time.delayedCall(750, () => this.summon()); - } else { - const trainerName = this.scene.currentBattle.trainer?.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER); - const pokemonName = this.getPokemon().getNameToRender(); - const message = i18next.t("battle:trainerSendOut", { trainerName, pokemonName }); - - this.scene.pbTrayEnemy.hide(); - this.scene.ui.showText(message, null, () => this.summon()); - } - } - - summon(): void { - const pokemon = this.getPokemon(); - - const pokeball = this.scene.addFieldSprite(this.player ? 36 : 248, this.player ? 80 : 44, "pb", getPokeballAtlasKey(pokemon.pokeball)); - pokeball.setVisible(false); - pokeball.setOrigin(0.5, 0.625); - this.scene.field.add(pokeball); - - if (this.fieldIndex === 1) { - pokemon.setFieldPosition(FieldPosition.RIGHT, 0); - } else { - const availablePartyMembers = this.getParty().filter(p => p.isAllowedInBattle()).length; - pokemon.setFieldPosition(!this.scene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT); - } - - const fpOffset = pokemon.getFieldPositionOffset(); - - pokeball.setVisible(true); - - this.scene.tweens.add({ - targets: pokeball, - duration: 650, - x: (this.player ? 100 : 236) + fpOffset[0] - }); - - this.scene.tweens.add({ - targets: pokeball, - duration: 150, - ease: "Cubic.easeOut", - y: (this.player ? 70 : 34) + fpOffset[1], - onComplete: () => { - this.scene.tweens.add({ - targets: pokeball, - duration: 500, - ease: "Cubic.easeIn", - angle: 1440, - y: (this.player ? 132 : 86) + fpOffset[1], - onComplete: () => { - this.scene.playSound("pb_rel"); - pokeball.destroy(); - this.scene.add.existing(pokemon); - this.scene.field.add(pokemon); - if (!this.player) { - const playerPokemon = this.scene.getPlayerPokemon() as Pokemon; - if (playerPokemon?.visible) { - this.scene.field.moveBelow(pokemon, playerPokemon); - } - this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); - } - addPokeballOpenParticles(this.scene, pokemon.x, pokemon.y - 16, pokemon.pokeball); - this.scene.updateModifiers(this.player); - this.scene.updateFieldScale(); - pokemon.showInfo(); - pokemon.playAnim(); - pokemon.setVisible(true); - pokemon.getSprite().setVisible(true); - pokemon.setScale(0.5); - pokemon.tint(getPokeballTintColor(pokemon.pokeball)); - pokemon.untint(250, "Sine.easeIn"); - this.scene.updateFieldScale(); - this.scene.tweens.add({ - targets: pokemon, - duration: 250, - ease: "Sine.easeIn", - scale: pokemon.getSpriteScale(), - onComplete: () => { - pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); - pokemon.getSprite().clearTint(); - pokemon.resetSummonData(); - this.scene.time.delayedCall(1000, () => this.end()); - } - }); - } - }); - } - }); - } - - onEnd(): void { - const pokemon = this.getPokemon(); - - if (pokemon.isShiny()) { - this.scene.unshiftPhase(new ShinySparklePhase(this.scene, pokemon.getBattlerIndex())); - } - - pokemon.resetTurnData(); - - if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1) { - this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); - this.queuePostSummon(); - } - } - - queuePostSummon(): void { - this.scene.pushPhase(new PostSummonPhase(this.scene, this.getPokemon().getBattlerIndex())); - } - - end() { - this.onEnd(); - - super.end(); - } -} -//#endregion - - - - - -//#region 23 SwitchSummonPhase -export class SwitchSummonPhase extends SummonPhase { - private slotIndex: integer; - private doReturn: boolean; - private batonPass: boolean; - - private lastPokemon: Pokemon; - - /** - * Constructor for creating a new SwitchSummonPhase - * @param scene {@linkcode BattleScene} the scene the phase is associated with - * @param fieldIndex integer representing position on the battle field - * @param slotIndex integer for the index of pokemon (in party of 6) to switch into - * @param doReturn boolean whether to render "comeback" dialogue - * @param batonPass boolean if the switch is from baton pass - * @param player boolean if the switch is from the player - */ - constructor(scene: BattleScene, fieldIndex: integer, slotIndex: integer, doReturn: boolean, batonPass: boolean, player?: boolean) { - super(scene, fieldIndex, player !== undefined ? player : true); - - this.slotIndex = slotIndex; - this.doReturn = doReturn; - this.batonPass = batonPass; - } - - start(): void { - super.start(); - } - - preSummon(): void { - if (!this.player) { - if (this.slotIndex === -1) { - //@ts-ignore - this.slotIndex = this.scene.currentBattle.trainer?.getNextSummonIndex(!this.fieldIndex ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER); // TODO: what would be the default trainer-slot fallback? - } - if (this.slotIndex > -1) { - this.showEnemyTrainer(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER); - this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); - } - } - - if (!this.doReturn || (this.slotIndex !== -1 && !(this.player ? this.scene.getParty() : this.scene.getEnemyParty())[this.slotIndex])) { - if (this.player) { - return this.switchAndSummon(); - } else { - this.scene.time.delayedCall(750, () => this.switchAndSummon()); - return; - } - } - - const pokemon = this.getPokemon(); - - if (!this.batonPass) { - (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); - } - - this.scene.ui.showText(this.player ? - i18next.t("battle:playerComeBack", { pokemonName: getPokemonNameWithAffix(pokemon) }) : - i18next.t("battle:trainerComeBack", { - trainerName: this.scene.currentBattle.trainer?.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER), - pokemonName: getPokemonNameWithAffix(pokemon) - }) - ); - this.scene.playSound("pb_rel"); - pokemon.hideInfo(); - pokemon.tint(getPokeballTintColor(pokemon.pokeball), 1, 250, "Sine.easeIn"); - this.scene.tweens.add({ - targets: pokemon, - duration: 250, - ease: "Sine.easeIn", - scale: 0.5, - onComplete: () => { - pokemon.leaveField(!this.batonPass, false); - this.scene.time.delayedCall(750, () => this.switchAndSummon()); - } - }); - } - - switchAndSummon() { - const party = this.player ? this.getParty() : this.scene.getEnemyParty(); - const switchedInPokemon = party[this.slotIndex]; - this.lastPokemon = this.getPokemon(); - applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon); - if (this.batonPass && switchedInPokemon) { - (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedInPokemon.id)); - if (!this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id)) { - const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier - && (m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id) as SwitchEffectTransferModifier; - if (batonPassModifier && !this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id)) { - this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedInPokemon, false); - } - } - } - if (switchedInPokemon) { - party[this.slotIndex] = this.lastPokemon; - party[this.fieldIndex] = switchedInPokemon; - const showTextAndSummon = () => { - this.scene.ui.showText(this.player ? - i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(switchedInPokemon) }) : - i18next.t("battle:trainerGo", { - trainerName: this.scene.currentBattle.trainer?.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER), - pokemonName: this.getPokemon().getNameToRender() - }) - ); - // Ensure improperly persisted summon data (such as tags) is cleared upon switching - if (!this.batonPass) { - switchedInPokemon.resetBattleData(); - switchedInPokemon.resetSummonData(); - } - this.summon(); - }; - if (this.player) { - showTextAndSummon(); - } else { - this.scene.time.delayedCall(1500, () => { - this.hideEnemyTrainer(); - this.scene.pbTrayEnemy.hide(); - showTextAndSummon(); - }); - } - } else { - this.end(); - } - } - - onEnd(): void { - super.onEnd(); - - const pokemon = this.getPokemon(); - - const moveId = this.lastPokemon?.scene.currentBattle.lastMove; - const lastUsedMove = moveId ? allMoves[moveId] : undefined; - - const currentCommand = pokemon.scene.currentBattle.turnCommands[this.fieldIndex]?.command; - const lastPokemonIsForceSwitchedAndNotFainted = lastUsedMove?.hasAttr(ForceSwitchOutAttr) && !this.lastPokemon.isFainted(); - - // Compensate for turn spent summoning - // Or compensate for force switch move if switched out pokemon is not fainted - if (currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted) { - pokemon.battleSummonData.turnCount--; - } - - if (this.batonPass && pokemon) { - pokemon.transferSummon(this.lastPokemon); - } - - this.lastPokemon?.resetSummonData(); - - this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); - } - - queuePostSummon(): void { - this.scene.unshiftPhase(new PostSummonPhase(this.scene, this.getPokemon().getBattlerIndex())); - } -} -//#endregion - - - - - -//#region 24 ReturnPhase -export class ReturnPhase extends SwitchSummonPhase { - constructor(scene: BattleScene, fieldIndex: integer) { - super(scene, fieldIndex, -1, true, false); - } - - switchAndSummon(): void { - this.end(); - } - - summon(): void { } - - onEnd(): void { - const pokemon = this.getPokemon(); - - pokemon.resetTurnData(); - pokemon.resetSummonData(); - - this.scene.updateFieldScale(); - - this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger); - } -} -//#endregion - - - - - -//#region 25 ShowTrainerPhase -export class ShowTrainerPhase extends BattlePhase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - this.scene.trainer.setVisible(true); - - this.scene.trainer.setTexture(`trainer_${this.scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); - - this.scene.tweens.add({ - targets: this.scene.trainer, - x: 106, - duration: 1000, - onComplete: () => this.end() - }); - } -} -//#endregion - - - - - -//#region 26 ToggleDoublePositionPhase -export class ToggleDoublePositionPhase extends BattlePhase { - private double: boolean; - - constructor(scene: BattleScene, double: boolean) { - super(scene); - - this.double = double; - } - - start() { - super.start(); - - const playerPokemon = this.scene.getPlayerField().find(p => p.isActive(true)); - if (playerPokemon) { - playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { - if (playerPokemon.getFieldIndex() === 1) { - const party = this.scene.getParty(); - party[1] = party[0]; - party[0] = playerPokemon; - } - this.end(); - }); - } else { - this.end(); - } - } -} -//#endregion - - - - - -//#region 27 CheckSwitchPhase -export class CheckSwitchPhase extends BattlePhase { - protected fieldIndex: integer; - protected useName: boolean; - - constructor(scene: BattleScene, fieldIndex: integer, useName: boolean) { - super(scene); - - this.fieldIndex = fieldIndex; - this.useName = useName; - } - - start() { - super.start(); - - const pokemon = this.scene.getPlayerField()[this.fieldIndex]; - - if (this.scene.battleStyle === BattleStyle.SET) { - super.end(); - return; - } - - if (this.scene.field.getAll().indexOf(pokemon) === -1) { - this.scene.unshiftPhase(new SummonMissingPhase(this.scene, this.fieldIndex)); - super.end(); - return; - } - - if (!this.scene.getParty().slice(1).filter(p => p.isActive()).length) { - super.end(); - return; - } - - if (pokemon.getTag(BattlerTagType.FRENZY)) { - super.end(); - return; - } - - for (var i = 0; i < this.scene.getEnemyField().length; i++) { - var pk = this.scene.getEnemyField()[i] - var maxIVs: string[] = [] - var ivnames = ["HP", "Atk", "Def", "Sp.Atk", "Sp.Def", "Speed"] - pk.ivs.forEach((iv, j) => {if (iv == 31) maxIVs.push(ivnames[j])}) - var ivDesc = maxIVs.join(",") - if (ivDesc == "") { - ivDesc = "No Max IVs" - } else { - ivDesc = "31: " + ivDesc - } - pk.getBattleInfo().flyoutMenu.toggleFlyout(true) - pk.getBattleInfo().flyoutMenu.flyoutText[0].text = getNatureName(pk.nature) - pk.getBattleInfo().flyoutMenu.flyoutText[1].text = ivDesc - pk.getBattleInfo().flyoutMenu.flyoutText[2].text = pk.getAbility().name - pk.getBattleInfo().flyoutMenu.flyoutText[3].text = pk.getPassiveAbility().name - if (pk.hasAbility(pk.species.abilityHidden, true, true)) { - pk.getBattleInfo().flyoutMenu.flyoutText[2].setColor("#e8e8a8") - } - } - if (false) { - this.scene.pokemonInfoContainer.show(this.scene.getEnemyField()[0], false, 1, true); - if (this.scene.getEnemyField()[1] != undefined) { - this.scene.tweens.add({ - targets: this.scene.pokemonInfoContainer, - alpha: 1, - duration: 5000, - onComplete: () => { - this.scene.pokemonInfoContainer.hide(1.3) - this.scene.tweens.add({ - targets: this.scene.pokemonInfoContainer, - alpha: 1, - duration: 1000, - onComplete: () => { - this.scene.pokemonInfoContainer.show(this.scene.getEnemyField()[1], false, 1, true); - } - }) - } - }) - } - } - - for (var i = 0; i < this.scene.getEnemyField().length; i++) { - var pk = this.scene.getEnemyField()[i] - var maxIVs: string[] = [] - var ivnames = ["HP", "Atk", "Def", "Sp.Atk", "Sp.Def", "Speed"] - pk.ivs.forEach((iv, j) => {if (iv == 31) maxIVs.push(ivnames[j])}) - var ivDesc = maxIVs.join(",") - if (ivDesc == "") { - ivDesc = "No Max IVs" - } else { - ivDesc = "31: " + ivDesc - } - pk.getBattleInfo().flyoutMenu.toggleFlyout(true) - pk.getBattleInfo().flyoutMenu.flyoutText[0].text = getNatureName(pk.nature) - pk.getBattleInfo().flyoutMenu.flyoutText[1].text = ivDesc - pk.getBattleInfo().flyoutMenu.flyoutText[2].text = pk.getAbility().name - pk.getBattleInfo().flyoutMenu.flyoutText[3].text = pk.getPassiveAbility().name - if (pk.hasAbility(pk.species.abilityHidden, true, true)) { - pk.getBattleInfo().flyoutMenu.flyoutText[2].setColor("#e8e8a8") - } - } - if (false) { - this.scene.pokemonInfoContainer.show(this.scene.getEnemyField()[0], false, 1, true); - if (this.scene.getEnemyField()[1] != undefined) { - this.scene.tweens.add({ - targets: this.scene.pokemonInfoContainer, - alpha: 1, - duration: 5000, - onComplete: () => { - this.scene.pokemonInfoContainer.hide(1.3) - this.scene.tweens.add({ - targets: this.scene.pokemonInfoContainer, - alpha: 1, - duration: 1000, - onComplete: () => { - this.scene.pokemonInfoContainer.show(this.scene.getEnemyField()[1], false, 1, true); - } - }) - } - }) - } - } - - this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => { - this.scene.ui.setMode(Mode.CONFIRM, () => { - this.scene.ui.setMode(Mode.MESSAGE); - LoggerTools.isPreSwitch.value = true - this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); - this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, false, true)); - for (var i = 0; i < this.scene.getEnemyField().length; i++) { - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.toggleFlyout(false) - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[0].text = "???" - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[1].text = "???" - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[2].text = "???" - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[3].text = "???" - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[2].setColor("#f8f8f8") - this.scene.getEnemyField()[i].flyout.setText() - } - //this.scene.pokemonInfoContainer.hide() - this.end(); - }, () => { - this.scene.ui.setMode(Mode.MESSAGE); - for (var i = 0; i < this.scene.getEnemyField().length; i++) { - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.toggleFlyout(false) - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[0].text = "???" - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[1].text = "???" - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[2].text = "???" - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[3].text = "???" - this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.flyoutText[2].setColor("#f8f8f8") - } - //this.scene.pokemonInfoContainer.hide() - this.end(); - }); - }); - } -} -//#endregion - - - - - -//#region 28 SummonMissingPhase -export class SummonMissingPhase extends SummonPhase { - constructor(scene: BattleScene, fieldIndex: integer) { - super(scene, fieldIndex); - } - - preSummon(): void { - this.scene.ui.showText(i18next.t("battle:sendOutPokemon", { pokemonName: getPokemonNameWithAffix(this.getPokemon()) })); - this.scene.time.delayedCall(250, () => this.summon()); - } -} -//#endregion - - - - - -//#region 29 LevelCapPhase -export class LevelCapPhase extends FieldPhase { - constructor(scene: BattleScene) { - super(scene); - } - - start(): void { - super.start(); - - this.scene.ui.setMode(Mode.MESSAGE).then(() => { - this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:levelCapUp", { levelCap: this.scene.getMaxExpLevel() }), null, () => this.end(), null, true); - this.executeForAll(pokemon => pokemon.updateInfo(true)); - }); - } -} -//#endregion - - - - - -//#region 30 TurnInitPhase -export class TurnInitPhase extends FieldPhase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - // If the flyout was shown automatically, and the user hasn't made it go away, auto-hide it - this.scene.arenaFlyout.dismiss() - - this.scene.getPlayerField().forEach(p => { - // If this pokemon is in play and evolved into something illegal under the current challenge, force a switch - if (p.isOnField() && !p.isAllowedInBattle()) { - this.scene.queueMessage(i18next.t("challenges:illegalEvolution", { "pokemon": p.name }), null, true); - - const allowedPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); - - if (!allowedPokemon.length) { - // If there are no longer any legal pokemon in the party, game over. - this.scene.clearPhaseQueue(); - this.scene.unshiftPhase(new GameOverPhase(this.scene)); - } else if (allowedPokemon.length >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !allowedPokemon[0].isActive(true))) { - // If there is at least one pokemon in the back that is legal to switch in, force a switch. - p.switchOut(false); - } else { - // If there are no pokemon in the back but we're not game overing, just hide the pokemon. - // This should only happen in double battles. - p.leaveField(); - } - if (allowedPokemon.length === 1 && this.scene.currentBattle.double) { - this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); - } - } - }); - - //this.scene.pushPhase(new MoveAnimTestPhase(this.scene)); - this.scene.eventTarget.dispatchEvent(new TurnInitEvent()); - - LoggerTools.enemyPlan[0] = "" - LoggerTools.enemyPlan[1] = "" - LoggerTools.enemyPlan[2] = "" - LoggerTools.enemyPlan[3] = "" - - if (false) { - this.scene.getField().forEach((pokemon, i) => { - if (pokemon != undefined && pokemon != null) - console.log("Handle " + pokemon.name) - if (pokemon?.isActive()) { - if (pokemon.isPlayer()) { - this.scene.currentBattle.addParticipant(pokemon as PlayerPokemon); - } else { - console.log("Marked " + pokemon.name + " as used") - pokemon.usedInBattle = true; - pokemon.flyout.setText() - pokemon.getBattleInfo().iconsActive = true - } - pokemon.resetTurnData(); - this.scene.pushPhase(pokemon.isPlayer() ? new CommandPhase(this.scene, i) : new EnemyCommandPhase(this.scene, i - BattlerIndex.ENEMY)); - } - }); - } else { - this.scene.getField().forEach((pokemon, i) => { - if (pokemon?.isActive()) { - if (!pokemon.isPlayer()) { - pokemon.flyout.setText() - pokemon.usedInBattle = true; - pokemon.getBattleInfo().iconsActive = true - pokemon.resetTurnData(); - this.scene.pushPhase(pokemon.isPlayer() ? new CommandPhase(this.scene, i) : new EnemyCommandPhase(this.scene, i - BattlerIndex.ENEMY)); - } - } - }); - this.scene.getField().forEach((pokemon, i) => { - if (pokemon?.isActive()) { - if (pokemon.isPlayer()) { - this.scene.currentBattle.addParticipant(pokemon as PlayerPokemon); - pokemon.resetTurnData(); - this.scene.pushPhase(pokemon.isPlayer() ? new CommandPhase(this.scene, i) : new EnemyCommandPhase(this.scene, i - BattlerIndex.ENEMY)); - } - } - }); - } - - var Pt = this.scene.getEnemyParty() - var Pt1: EnemyPokemon[] = [] - var Pt2: EnemyPokemon[] = [] - for (var i = 0; i < Pt.length; i++) { - if (i % 2 == 0) { - Pt1.push(Pt[i]) - } else { - Pt2.push(Pt[i]) - } - } - Pt.forEach((pokemon, i) => { - if (pokemon != undefined && pokemon.hp > 0 && pokemon.isActive()) - if (pokemon.hasTrainer() || true) { - console.log(i) - if (pokemon.getFieldIndex() == 1 && pokemon.isOnField()) { - // Switch this to cycle between - // - hiding the top mon's team bar - // - showing the bottom mon's team bar with its active slots reversed - if (false) { - pokemon.getBattleInfo().displayParty(Pt) - Pt[0].getBattleInfo().switchIconVisibility(false); // Make the top mon's team bar go away - Pt[0].getBattleInfo().iconsActive = false; // Prevent the top mon from re-opening its bar - } else { - pokemon.getBattleInfo().displayParty(Pt2) - } - } else { - pokemon.getBattleInfo().displayParty((this.scene.currentBattle.double ? Pt1 : Pt)) - } - } - }) - - this.scene.pushPhase(new TurnStartPhase(this.scene)); - - this.scene.updateCatchRate() - - this.end(); - } -} -//#endregion - - - - - -//#region 31 CommandPhase -export class CommandPhase extends FieldPhase { - protected fieldIndex: integer; - - constructor(scene: BattleScene, fieldIndex: integer) { - super(scene); - - this.fieldIndex = fieldIndex; - } - - start() { - super.start(); - - if (this.fieldIndex) { - // If we somehow are attempting to check the right pokemon but there's only one pokemon out - // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching - if (this.scene.getPlayerField().filter(p => p.isActive()).length === 1) { - this.fieldIndex = FieldPosition.CENTER; - } else { - const allyCommand = this.scene.currentBattle.turnCommands[this.fieldIndex - 1]; - if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: allyCommand?.command, skip: true }; - } - } - } - - if (this.scene.currentBattle.turnCommands[this.fieldIndex]?.skip) { - return this.end(); - } - - const playerPokemon = this.scene.getPlayerField()[this.fieldIndex]; - - const moveQueue = playerPokemon.getMoveQueue(); - - while (moveQueue.length && moveQueue[0] - && moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move) - || !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move)]!.isUsable(playerPokemon, moveQueue[0].ignorePP))) { // TODO: is the bang correct? - moveQueue.shift(); - } - - if (moveQueue.length) { - const queuedMove = moveQueue[0]; - if (!queuedMove.move) { - this.handleCommand(Command.FIGHT, -1, false); - } else { - const moveIndex = playerPokemon.getMoveset().findIndex(m => m?.moveId === queuedMove.move); - if (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex]!.isUsable(playerPokemon, queuedMove.ignorePP)) { // TODO: is the bang correct? - this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, { targets: queuedMove.targets, multiple: queuedMove.targets.length > 1, isContinuing: true }); - } else { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - } - } - } else { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - } - } - - handleCommand(command: Command, cursor: integer, ...args: any[]): boolean { - const playerPokemon = this.scene.getPlayerField()[this.fieldIndex]; - const enemyField = this.scene.getEnemyField(); - let success: boolean; - - switch (command) { - case Command.FIGHT: - let useStruggle = false; - if (cursor === -1 || - playerPokemon.trySelectMove(cursor, args[0] as boolean) || - (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m?.isUsable(playerPokemon)).length)) { - const moveId = !useStruggle ? cursor > -1 ? playerPokemon.getMoveset()[cursor]!.moveId : Moves.NONE : Moves.STRUGGLE; // TODO: is the bang correct? - const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args }; - const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, moveId) : args[2]; - if (!moveId) { - turnCommand.targets = [this.fieldIndex]; - } - console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); - if (moveTargets.targets.length > 1 && moveTargets.multiple) { - this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)); - } - if (moveTargets.targets.length <= 1 || moveTargets.multiple) { - turnCommand.move!.targets = moveTargets.targets; //TODO: is the bang correct here? - } else if (playerPokemon.getTag(BattlerTagType.CHARGING) && playerPokemon.getMoveQueue().length >= 1) { - turnCommand.move!.targets = playerPokemon.getMoveQueue()[0].targets; //TODO: is the bang correct here? - } else { - this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)); - } - this.scene.currentBattle.turnCommands[this.fieldIndex] = turnCommand; - success = true; - } else if (cursor < playerPokemon.getMoveset().length) { - const move = playerPokemon.getMoveset()[cursor]!; //TODO: is this bang correct? - this.scene.ui.setMode(Mode.MESSAGE); - - // Decides between a Disabled, Not Implemented, or No PP translation message - const errorMessage = - playerPokemon.summonData.disabledMove === move.moveId ? "battle:moveDisabled" : - move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; - const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator - - this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => { - this.scene.ui.clearText(); - this.scene.ui.setMode(Mode.FIGHT, this.fieldIndex); - }, null, true); - } - break; - case Command.BALL: - const notInDex = (this.scene.getEnemyField().filter(p => p.isActive(true)).some(p => !p.scene.gameData.dexData[p.species.speciesId].caughtAttr) && this.scene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarters).length - 1); - if (this.scene.arena.biomeType === Biome.END && (!this.scene.gameMode.isClassic || this.scene.gameMode.isFreshStartChallenge() || notInDex )) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t("battle:noPokeballForce"), null, () => { - this.scene.ui.showText("", 0); - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - }, null, true); - } else if (this.scene.currentBattle.battleType === BattleType.TRAINER) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t("battle:noPokeballTrainer"), null, () => { - this.scene.ui.showText("", 0); - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - }, null, true); - } else { - const targets = this.scene.getEnemyField().filter(p => p.isActive(true)).map(p => p.getBattlerIndex()); - if (targets.length > 1) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t("battle:noPokeballMulti"), null, () => { - this.scene.ui.showText("", 0); - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - }, null, true); - } else if (cursor < 5) { - const targetPokemon = this.scene.getEnemyField().find(p => p.isActive(true)); - if (targetPokemon?.isBoss() && targetPokemon?.bossSegmentIndex >= 1 && !targetPokemon?.hasAbility(Abilities.WONDER_GUARD, false, true) && cursor < PokeballType.MASTER_BALL) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t("battle:noPokeballStrong"), null, () => { - this.scene.ui.showText("", 0); - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - }, null, true); - } else { - this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.BALL, cursor: cursor }; - this.scene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets; - if (this.fieldIndex) { - this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; - } - success = true; - } - } - } - break; - case Command.POKEMON: - case Command.RUN: - const isSwitch = command === Command.POKEMON; - if (!isSwitch && this.scene.arena.biomeType === Biome.END) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t("battle:noEscapeForce"), null, () => { - this.scene.ui.showText("", 0); - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - }, null, true); - } else if (!isSwitch && this.scene.currentBattle.battleType === BattleType.TRAINER) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t("battle:noEscapeTrainer"), null, () => { - this.scene.ui.showText("", 0); - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - }, null, true); - } else { - const trapTag = playerPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag; - const trapped = new Utils.BooleanHolder(false); - const batonPass = isSwitch && args[0] as boolean; - const trappedAbMessages: string[] = []; - if (!batonPass) { - enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped, playerPokemon, true, trappedAbMessages)); - } - if (batonPass || (!trapTag && !trapped.value)) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch - ? { command: Command.POKEMON, cursor: cursor, args: args } - : { command: Command.RUN }; - success = true; - if (!isSwitch && this.fieldIndex) { - this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; - } - } else if (trapTag) { - if (trapTag.sourceMove === Moves.INGRAIN && trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId)?.isOfType(Type.GHOST)) { - success = true; - this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch - ? { command: Command.POKEMON, cursor: cursor, args: args } - : { command: Command.RUN }; - break; - } - if (!isSwitch) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - this.scene.ui.setMode(Mode.MESSAGE); - } - this.scene.ui.showText( - i18next.t("battle:noEscapePokemon", { - pokemonName: trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId) ? getPokemonNameWithAffix(this.scene.getPokemonById(trapTag.sourceId)!) : "", - moveName: trapTag.getMoveName(), - escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee") - }), - null, - () => { - this.scene.ui.showText("", 0); - if (!isSwitch) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - } - }, null, true); - } else if (trapped.value && trappedAbMessages.length > 0) { - if (!isSwitch) { - this.scene.ui.setMode(Mode.MESSAGE); - } - this.scene.ui.showText(trappedAbMessages[0], null, () => { - this.scene.ui.showText("", 0); - if (!isSwitch) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - } - }, null, true); - } - } - break; - } - - if (success!) { // TODO: is the bang correct? - this.end(); - } - - return success!; // TODO: is the bang correct? - } - - cancel() { - if (this.fieldIndex) { - this.scene.unshiftPhase(new CommandPhase(this.scene, 0)); - this.scene.unshiftPhase(new CommandPhase(this.scene, 1)); - this.end(); - } - } - - checkFightOverride(): boolean { - const pokemon = this.getPokemon(); - - const encoreTag = pokemon.getTag(EncoreTag) as EncoreTag; - - if (!encoreTag) { - return false; - } - - const moveIndex = pokemon.getMoveset().findIndex(m => m?.moveId === encoreTag.moveId); - - if (moveIndex === -1 || !pokemon.getMoveset()[moveIndex]!.isUsable(pokemon)) { // TODO: is this bang correct? - return false; - } - - this.handleCommand(Command.FIGHT, moveIndex, false); - - return true; - } - - getFieldIndex(): integer { - return this.fieldIndex; - } - - getPokemon(): PlayerPokemon { - return this.scene.getPlayerField()[this.fieldIndex]; - } - - end() { - this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); - } -} -//#endregion - - - - - -//#region 32 EnemyCommandPhase -/** - * Phase for determining an enemy AI's action for the next turn. - * During this phase, the enemy decides whether to switch (if it has a trainer) - * or to use a move from its moveset. - * - * For more information on how the Enemy AI works, see docs/enemy-ai.md - * @see {@linkcode Pokemon.getMatchupScore} - * @see {@linkcode EnemyPokemon.getNextMove} - */ -export class EnemyCommandPhase extends FieldPhase { - protected fieldIndex: integer; - - constructor(scene: BattleScene, fieldIndex: integer) { - super(scene); - - this.fieldIndex = fieldIndex; - } - - start() { - super.start(); - - const enemyPokemon = this.scene.getEnemyField()[this.fieldIndex]; - console.log(enemyPokemon.getMoveset().map(m => m?.getName())) - - const battle = this.scene.currentBattle; - - const trainer = battle.trainer; - - /** - * If the enemy has a trainer, decide whether or not the enemy should switch - * to another member in its party. - * - * This block compares the active enemy Pokemon's {@linkcode Pokemon.getMatchupScore | matchup score} - * against the active player Pokemon with the enemy party's other non-fainted Pokemon. If a party - * member's matchup score is 3x the active enemy's score (or 2x for "boss" trainers), - * the enemy will switch to that Pokemon. - */ - if (trainer && !enemyPokemon.getMoveQueue().length) { - const opponents = enemyPokemon.getOpponents(); - - const trapTag = enemyPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag; - const trapped = new Utils.BooleanHolder(false); - opponents.forEach(playerPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, playerPokemon, trapped, enemyPokemon, true, [])); - if (!trapTag && !trapped.value) { - const partyMemberScores = trainer.getPartyMemberMatchupScores(enemyPokemon.trainerSlot, true); - - if (partyMemberScores.length) { - const matchupScores = opponents.map(opp => enemyPokemon.getMatchupScore(opp)); - const matchupScore = matchupScores.reduce((total, score) => total += score, 0) / matchupScores.length; - - const sortedPartyMemberScores = trainer.getSortedPartyMemberMatchupScores(partyMemberScores); - - const switchMultiplier = 1 - (battle.enemySwitchCounter ? Math.pow(0.1, (1 / battle.enemySwitchCounter)) : 0); - - if (sortedPartyMemberScores[0][1] * switchMultiplier >= matchupScore * (trainer.config.isBoss ? 2 : 3)) { - const index = trainer.getNextSummonIndex(enemyPokemon.trainerSlot, partyMemberScores); - - battle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.POKEMON, cursor: index, args: [false] }; - console.log(enemyPokemon.name + " selects:", "Switch to " + this.scene.getEnemyParty()[index].name) - battle.enemySwitchCounter++; - - LoggerTools.enemyPlan[this.fieldIndex*2] = "Switching out" - LoggerTools.enemyPlan[this.fieldIndex*2 + 1] = "→ " + this.scene.getEnemyParty()[index].name - - enemyPokemon.flyout.setText() - - this.scene.updateCatchRate() - - return this.end(); - } - } - } - } - - /** Select a move to use (and a target to use it against, if applicable) */ - const nextMove = enemyPokemon.getNextMove(); - const mv = new PokemonMove(nextMove.move) - - this.scene.currentBattle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.FIGHT, move: nextMove }; - const targetLabels = ["Counter", "[PLAYER L]", "[PLAYER R]", "[ENEMY L]", "[ENEMY R]"] - this.scene.getParty().forEach((v, i, a) => { - if (v.isActive() && v.name) { - targetLabels[i + 1] = v.name - } - }) - this.scene.getEnemyParty().forEach((v, i, a) => { - if (v.isActive() && v.name) { - targetLabels[i + 3] = v.name - } - }) - if (this.fieldIndex == 0) { - targetLabels[3] = "Self" - } - if (this.fieldIndex == 1) { - targetLabels[4] = "Self" - } - if (targetLabels[1] == targetLabels[2]) { - targetLabels[1] += " (L)" - targetLabels[2] += " (R)" - } - console.log(enemyPokemon.name + " selects:", mv.getName() + " → " + nextMove.targets.map((m) => targetLabels[m + 1])) - this.scene.currentBattle.enemySwitchCounter = Math.max(this.scene.currentBattle.enemySwitchCounter - 1, 0); - - LoggerTools.enemyPlan[this.fieldIndex*2] = mv.getName() - LoggerTools.enemyPlan[this.fieldIndex*2 + 1] = "→ " + nextMove.targets.map((m) => targetLabels[m + 1]) - this.scene.arenaFlyout.updateFieldText() - - this.scene.updateCatchRate() - - this.end(); - } -} -export function enemyTurnCalc(scene: BattleScene) { - scene.getField().forEach(pokemon => { - if (pokemon.isActive()) { - if (!pokemon.isPlayer()) { - var pk = pokemon as EnemyPokemon; - } - } - }) -} -//#endregion - - - - - -//#region 33 SelectTargetPhase -export class SelectTargetPhase extends PokemonPhase { - constructor(scene: BattleScene, fieldIndex: integer) { - super(scene, fieldIndex); - } - - start() { - super.start(); - - const turnCommand = this.scene.currentBattle.turnCommands[this.fieldIndex]; - const move = turnCommand?.move?.move; - this.scene.ui.setMode(Mode.TARGET_SELECT, this.fieldIndex, move, (targets: BattlerIndex[]) => { - this.scene.ui.setMode(Mode.MESSAGE); - if (targets.length < 1) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = null; - this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex)); - } else { - turnCommand!.targets = targets; //TODO: is the bang correct here? - } - if (turnCommand?.command === Command.BALL && this.fieldIndex) { - this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; //TODO: is the bang correct here? - } - this.end(); - }); - } -} -//#endregion - - - - - -//#region 34 TurnStartPhase -export class TurnStartPhase extends FieldPhase { - constructor(scene: BattleScene) { - super(scene); - } - - generateTargString(t: BattlerIndex[]) { - var targets = ['Self'] - for (var i = 0; i < this.scene.getField().length; i++) { - if (this.scene.getField()[i] != null) - targets[this.scene.getField()[i].getBattlerIndex() + 1] = this.scene.getField()[i].name - } - for (var i = 0; i < this.scene.getEnemyField().length; i++) { - if (this.scene.getEnemyField()[i] != null) - targets[this.scene.getEnemyField()[i].getBattlerIndex() + 1] = this.scene.getEnemyField()[i].name - } - var targetFull: string[] = [] - for (var i = 0; i < t.length; i++) { - targetFull.push(targets[t[i] + 1]) - } - if (targetFull.join(", ") == targets.join(", ")) return "" - return " → " + targetFull.join(", ") - } - - getBattlers(user: Pokemon): Pokemon[] { - var battlers: Pokemon[] = [] - battlers[0] = this.scene.getField()[0] - battlers[1] = this.scene.getField()[1] - battlers[2] = this.scene.getEnemyField()[0] - battlers[3] = this.scene.getEnemyField()[1] - battlers.unshift(user) - return battlers; - } - - start() { - super.start(); - - const field = this.scene.getField(); - const order = this.getOrder(); - - const battlerBypassSpeed = {}; - - const playerActions: string[] = [] - - const moveOrder = order.slice(0); - - while (LoggerTools.Actions.length > 0) { - LoggerTools.Actions.pop() - } - - for (const o of moveOrder) { - - const pokemon = field[o]; - const turnCommand = this.scene.currentBattle.turnCommands[o]; - - if (turnCommand?.skip || !pokemon.isPlayer()) { - continue; - } - - switch (turnCommand?.command) { - case Command.FIGHT: - const queuedMove = turnCommand.move; - if (!queuedMove) { - continue; - } - LoggerTools.Actions[pokemon.getBattlerIndex()] = `[[ ${new PokemonMove(queuedMove.move).getName()} unknown target ]]` - break; - case Command.BALL: - var ballNames = [ - "Poké Ball", - "Great Ball", - "Ultra Ball", - "Rogue Ball", - "Master Ball", - "Luxury Ball" - ] - LoggerTools.Actions[pokemon.getBattlerIndex()] = ballNames[turnCommand.cursor!] - playerActions.push(ballNames[turnCommand.cursor!]) - //this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets[0] % 2, turnCommand.cursor)); - break; - case Command.POKEMON: - break; - case Command.RUN: - LoggerTools.Actions[pokemon.getBattlerIndex()] = "Run" - playerActions.push("Run") - break; - } - } - //LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, playerActions.join(" | ")) - - this.scene.getField(true).filter(p => p.summonData).map(p => { - const bypassSpeed = new Utils.BooleanHolder(false); - const canCheckHeldItems = new Utils.BooleanHolder(true); - applyAbAttrs(BypassSpeedChanceAbAttr, p, null, bypassSpeed); - applyAbAttrs(PreventBypassSpeedChanceAbAttr, p, null, bypassSpeed, canCheckHeldItems); - if (canCheckHeldItems.value) { - this.scene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); - } - battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; - }); - - moveOrder.sort((a, b) => { - const aCommand = this.scene.currentBattle.turnCommands[a]; - const bCommand = this.scene.currentBattle.turnCommands[b]; - - if (aCommand?.command !== bCommand?.command) { - if (aCommand?.command === Command.FIGHT) { - return 1; - } else if (bCommand?.command === Command.FIGHT) { - return -1; - } - } else if (aCommand?.command === Command.FIGHT) { - const aMove = allMoves[aCommand.move!.move];//TODO: is the bang correct here? - const bMove = allMoves[bCommand!.move!.move];//TODO: is the bang correct here? - - const aPriority = new Utils.IntegerHolder(aMove.priority); - const bPriority = new Utils.IntegerHolder(bMove.priority); - - applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority); //TODO: is the bang correct here? - applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority); //TODO: is the bang correct here? - - applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority); //TODO: is the bang correct here? - applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority); //TODO: is the bang correct here? - - if (aPriority.value !== bPriority.value) { - const bracketDifference = Math.ceil(aPriority.value) - Math.ceil(bPriority.value); - const hasSpeedDifference = battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value; - if (bracketDifference === 0 && hasSpeedDifference) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - return aPriority.value < bPriority.value ? 1 : -1; - } - } - - if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - - const aIndex = order.indexOf(a); - const bIndex = order.indexOf(b); - - return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; - }); - - let orderIndex = 0; - - for (const o of moveOrder) { - - const pokemon = field[o]; - const turnCommand = this.scene.currentBattle.turnCommands[o]; - - if (turnCommand?.skip) { - continue; - } - - switch (turnCommand?.command) { - case Command.FIGHT: - const queuedMove = turnCommand.move; - pokemon.turnData.order = orderIndex++; - if (!queuedMove) { - continue; - } - const move = pokemon.getMoveset().find(m => m?.moveId === queuedMove.move) || new PokemonMove(queuedMove.move); - if (move.getMove().hasAttr(MoveHeaderAttr)) { - this.scene.unshiftPhase(new MoveHeaderPhase(this.scene, pokemon, move)); - } - if (pokemon.isPlayer()) { - if (turnCommand.cursor === -1) { - //console.log("turncommand cursor was -1 -- running TOP block") - this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move));//TODO: is the bang correct here? - var targets = turnCommand.targets || turnCommand.move!.targets - var mv = move - if (pokemon.isPlayer()) { - console.log(turnCommand.targets, turnCommand.move!.targets) - 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) - } - } else { - //console.log("turncommand = ", turnCommand, " -- running BOTTOM block") - const playerPhase = new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move, false, queuedMove.ignorePP);//TODO: is the bang correct here? - var targets = turnCommand.targets || turnCommand.move!.targets - var mv = move - 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) - } - } - this.scene.pushPhase(playerPhase); - } - } else { - this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move, false, queuedMove.ignorePP));//TODO: is the bang correct here? - var targets = turnCommand.targets || turnCommand.move!.targets - var mv = new PokemonMove(queuedMove.move) - } - break; - case Command.BALL: - this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets![0] % 2, turnCommand.cursor!));//TODO: is the bang correct here? - break; - case Command.POKEMON: - 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!) - } - 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? - break; - case Command.RUN: - let runningPokemon = pokemon; - if (this.scene.currentBattle.double) { - const playerActivePokemon = field.filter(pokemon => { - if (!!pokemon) { - return pokemon.isPlayer() && pokemon.isActive(); - } else { - return; - } - }); - // if only one pokemon is alive, use that one - if (playerActivePokemon.length > 1) { - // find which active pokemon has faster speed - const fasterPokemon = playerActivePokemon[0].getStat(Stat.SPD) > playerActivePokemon[1].getStat(Stat.SPD) ? playerActivePokemon[0] : playerActivePokemon[1]; - // check if either active pokemon has the ability "Run Away" - const hasRunAway = playerActivePokemon.find(p => p.hasAbility(Abilities.RUN_AWAY)); - runningPokemon = hasRunAway !== undefined ? hasRunAway : fasterPokemon; - } - } - this.scene.unshiftPhase(new AttemptRunPhase(this.scene, runningPokemon.getFieldIndex())); - break; - } - } - - - this.scene.pushPhase(new WeatherEffectPhase(this.scene)); - - for (const o of order) { - if (field[o].status && field[o].status.isPostTurn()) { - this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, o)); - } - } - - this.scene.pushPhase(new BerryPhase(this.scene)); - this.scene.pushPhase(new TurnEndPhase(this.scene)); - - 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 - } - if (LoggerTools.Actions.length > 1 && (LoggerTools.Actions[0] == "" || LoggerTools.Actions[0] == undefined || LoggerTools.Actions[0] == null)) - LoggerTools.Actions.shift() // If the left slot isn't doing anything, delete its entry - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, LoggerTools.Actions.join(" & ")) - - /** - * this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front - * of the queue and dequeues to start the next phase - * this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence - */ - this.end(); - } -} -//#endregion - - - - - -//#region 35 BerryPhase -/** The phase after attacks where the pokemon eat berries */ -export class BerryPhase extends FieldPhase { - start() { - super.start(); - - this.executeForAll((pokemon) => { - const hasUsableBerry = !!this.scene.findModifier((m) => { - return m instanceof BerryModifier && m.shouldApply([pokemon]); - }, pokemon.isPlayer()); - - if (hasUsableBerry) { - const cancelled = new Utils.BooleanHolder(false); - pokemon.getOpponents().map((opp) => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled)); - - if (cancelled.value) { - pokemon.scene.queueMessage(i18next.t("abilityTriggers:preventBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); - } else { - this.scene.unshiftPhase( - new CommonAnimPhase(this.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM) - ); - - for (const berryModifier of this.scene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon) as BerryModifier[]) { - if (berryModifier.consumed) { - if (!--berryModifier.stackCount) { - this.scene.removeModifier(berryModifier); - } else { - berryModifier.consumed = false; - } - } - this.scene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used - } - - this.scene.updateModifiers(pokemon.isPlayer()); - - applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new Utils.BooleanHolder(false)); - } - } - }); - - this.end(); - } -} -//#endregion - - - - - -//#region 36 TurnEndPhase -export class TurnEndPhase extends FieldPhase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - this.scene.arenaFlyout.updateFieldText() - - this.scene.currentBattle.incrementTurn(this.scene); - this.scene.eventTarget.dispatchEvent(new TurnEndEvent(this.scene.currentBattle.turn)); - - const handlePokemon = (pokemon: Pokemon) => { - pokemon.lapseTags(BattlerTagLapseType.TURN_END); - - if (pokemon.summonData.disabledMove && !--pokemon.summonData.disabledTurns) { - this.scene.pushPhase(new MessagePhase(this.scene, i18next.t("battle:notDisabled", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: allMoves[pokemon.summonData.disabledMove].name }))); - pokemon.summonData.disabledMove = Moves.NONE; - } - - this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon); - - if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { - this.scene.unshiftPhase(new PokemonHealPhase(this.scene, pokemon.getBattlerIndex(), - Math.max(pokemon.getMaxHp() >> 4, 1), i18next.t("battle:turnEndHpRestore", { pokemonName: getPokemonNameWithAffix(pokemon) }), true)); - } - - if (!pokemon.isPlayer()) { - this.scene.applyModifiers(EnemyTurnHealModifier, false, pokemon); - this.scene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon); - } - - applyPostTurnAbAttrs(PostTurnAbAttr, pokemon); - - this.scene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); - - this.scene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); - - pokemon.battleSummonData.turnCount++; - }; - - this.executeForAll(handlePokemon); - - this.scene.arena.lapseTags(); - - if (this.scene.arena.weather && !this.scene.arena.weather.lapse()) { - this.scene.arena.trySetWeather(WeatherType.NONE, false); - } - - if (this.scene.arena.terrain && !this.scene.arena.terrain.lapse()) { - this.scene.arena.trySetTerrain(TerrainType.NONE, false); - } - - this.end(); - } -} -//#endregion - - - - - -//#region 37 BattleEndPhase -export class BattleEndPhase extends BattlePhase { - start() { - super.start(); - - this.scene.currentBattle.addBattleScore(this.scene); - - this.scene.gameData.gameStats.battles++; - if (this.scene.currentBattle.trainer) { - this.scene.gameData.gameStats.trainersDefeated++; - } - if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) { - this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1; - } - - // Endless graceful end - if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex >= 5850) { - this.scene.clearPhaseQueue(); - this.scene.unshiftPhase(new GameOverPhase(this.scene, true)); - } - - for (const pokemon of this.scene.getField()) { - if (pokemon) { - pokemon.resetBattleSummonData(); - } - } - - for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) { - applyPostBattleAbAttrs(PostBattleAbAttr, pokemon); - } - - if (this.scene.currentBattle.moneyScattered) { - this.scene.currentBattle.pickUpScatteredMoney(this.scene); - } - - this.scene.clearEnemyHeldItemModifiers(); - - const lapsingModifiers = this.scene.findModifiers(m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[]; - for (const m of lapsingModifiers) { - const args: any[] = []; - if (m instanceof LapsingPokemonHeldItemModifier) { - args.push(this.scene.getPokemonById(m.pokemonId)); - } - if (!m.lapse(args)) { - this.scene.removeModifier(m); - } - } - - var drpd: LoggerTools.DRPD = LoggerTools.getDRPD(this.scene) - var wv: LoggerTools.Wave = LoggerTools.getWave(drpd, this.scene.currentBattle.waveIndex, this.scene) - var lastcount = 0; - var lastval; - var tempActions: string[] = wv.actions.slice(); - var prevWaveActions: string[] = [] - wv.actions = [] - // Loop through each action - for (var i = 0; i < tempActions.length; i++) { - if (tempActions[i].substring(0, 10) == "[MOVEBACK]") { - prevWaveActions.push(tempActions[i].substring(10)) - } else if (tempActions[i] != lastval) { - if (lastcount > 0) { - wv.actions.push(lastval + (lastcount == 1 ? "" : " x" + lastcount)) - } - lastval = tempActions[i] - lastcount = 1 - } else { - lastcount++ - } - } - if (lastcount > 0) { - wv.actions.push(lastval + (lastcount == 1 ? "" : " x" + lastcount)) - } - console.log(tempActions, wv.actions) - var wv2: LoggerTools.Wave = LoggerTools.getWave(drpd, this.scene.currentBattle.waveIndex - 1, this.scene) - wv2.actions = wv2.actions.concat(prevWaveActions) - console.log(drpd) - LoggerTools.save(this.scene, drpd) - - this.scene.updateModifiers().then(() => this.end()); - } -} -//#endregion - - - - - -//#region 38 NewBattlePhase -export class NewBattlePhase extends BattlePhase { - start() { - super.start(); - - this.scene.newBattle(); - - this.end(); - } -} -//#endregion - - - - - -//#region 39 CommonAnimPhase -export class CommonAnimPhase extends PokemonPhase { - private anim: CommonAnim | null; - private targetIndex: integer | undefined; - - constructor(scene: BattleScene, battlerIndex?: BattlerIndex, targetIndex?: BattlerIndex | undefined, anim?: CommonAnim) { - super(scene, battlerIndex); - - this.anim = anim!; // TODO: is this bang correct? - this.targetIndex = targetIndex; - } - - setAnimation(anim: CommonAnim) { - this.anim = anim; - } - - start() { - new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, () => { - this.end(); - }); - } -} - -export class MoveHeaderPhase extends BattlePhase { - public pokemon: Pokemon; - public move: PokemonMove; - - constructor(scene: BattleScene, pokemon: Pokemon, move: PokemonMove) { - super(scene); - - this.pokemon = pokemon; - this.move = move; - } - - canMove(): boolean { - return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon); - } - - start() { - super.start(); - - if (this.canMove()) { - applyMoveAttrs(MoveHeaderAttr, this.pokemon, null, this.move.getMove()).then(() => this.end()); - } else { - this.end(); - } - } -} -//#endregion - - - - - -//#region 40 MovePhase -export class MovePhase extends BattlePhase { - public pokemon: Pokemon; - public move: PokemonMove; - public targets: BattlerIndex[]; - protected followUp: boolean; - protected ignorePp: boolean; - protected failed: boolean; - protected cancelled: boolean; - - constructor(scene: BattleScene, pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp?: boolean, ignorePp?: boolean) { - super(scene); - - this.pokemon = pokemon; - this.targets = targets; - this.move = move; - this.followUp = !!followUp; - this.ignorePp = !!ignorePp; - this.failed = false; - this.cancelled = false; - } - - canMove(): boolean { - return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp) && !!this.targets.length; - } - - /**Signifies the current move should fail but still use PP */ - fail(): void { - this.failed = true; - } - - /**Signifies the current move should cancel and retain PP */ - cancel(): void { - this.cancelled = true; - } - - start() { - super.start(); - - console.log(Moves[this.move.moveId]); - - if (!this.canMove()) { - if (this.move.moveId && this.pokemon.summonData?.disabledMove === this.move.moveId) { - this.scene.queueMessage(`${this.move.getName()} is disabled!`); - } - if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails - this.fail(); - this.showMoveText(); - this.showFailedText(); - } - return this.end(); - } - - if (!this.followUp) { - if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) { - this.scene.arena.setIgnoreAbilities(); - } - } else { - this.pokemon.turnData.hitsLeft = 0; // TODO: is `0` correct? - this.pokemon.turnData.hitCount = 0; // TODO: is `0` correct? - } - - // Move redirection abilities (ie. Storm Drain) only support single target moves - const moveTarget = this.targets.length === 1 - ? new Utils.IntegerHolder(this.targets[0]) - : null; - if (moveTarget) { - const oldTarget = moveTarget.value; - this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, this.move.moveId, moveTarget)); - this.pokemon.getOpponents().forEach(p => { - const redirectTag = p.getTag(CenterOfAttentionTag) as CenterOfAttentionTag; - if (redirectTag && (!redirectTag.powder || (!this.pokemon.isOfType(Type.GRASS) && !this.pokemon.hasAbility(Abilities.OVERCOAT)))) { - moveTarget.value = p.getBattlerIndex(); - } - }); - //Check if this move is immune to being redirected, and restore its target to the intended target if it is. - if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().hasAttr(BypassRedirectAttr))) { - //If an ability prevented this move from being redirected, display its ability pop up. - if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().hasAttr(BypassRedirectAttr)) && oldTarget !== moveTarget.value) { - this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr))); - } - moveTarget.value = oldTarget; - } - this.targets[0] = moveTarget.value; - } - - // Check for counterattack moves to switch target - if (this.targets.length === 1 && this.targets[0] === BattlerIndex.ATTACKER) { - if (this.pokemon.turnData.attacksReceived.length) { - const attack = this.pokemon.turnData.attacksReceived[0]; - this.targets[0] = attack.sourceBattlerIndex; - - // account for metal burst and comeuppance hitting remaining targets in double battles - // counterattack will redirect to remaining ally if original attacker faints - if (this.scene.currentBattle.double && this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER)) { - if (this.scene.getField()[this.targets[0]].hp === 0) { - const opposingField = this.pokemon.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField(); - //@ts-ignore - this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex(); //TODO: fix ts-ignore - } - } - } - if (this.targets[0] === BattlerIndex.ATTACKER) { - this.fail(); // Marks the move as failed for later in doMove - this.showMoveText(); - this.showFailedText(); - } - } - - const targets = this.scene.getField(true).filter(p => { - if (this.targets.indexOf(p.getBattlerIndex()) > -1) { - return true; - } - return false; - }); - - const doMove = () => { - this.pokemon.turnData.acted = true; // Record that the move was attempted, even if it fails - - this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE); - - let ppUsed = 1; - // Filter all opponents to include only those this move is targeting - const targetedOpponents = this.pokemon.getOpponents().filter(o => this.targets.includes(o.getBattlerIndex())); - for (const opponent of targetedOpponents) { - if (this.move.ppUsed + ppUsed >= this.move.getMovePp()) { // If we're already at max PP usage, stop checking - break; - } - if (opponent.hasAbilityWithAttr(IncreasePpAbAttr)) { // Accounting for abilities like Pressure - ppUsed++; - } - } - - if (!this.followUp && this.canMove() && !this.cancelled) { - this.pokemon.lapseTags(BattlerTagLapseType.MOVE); - } - - const moveQueue = this.pokemon.getMoveQueue(); - if (this.cancelled || this.failed) { - if (this.failed) { - this.move.usePp(ppUsed); // Only use PP if the move failed - this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed)); - } - - // Record a failed move so Abilities like Truant don't trigger next turn and soft-lock - this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); - - this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc. - moveQueue.shift(); // Remove the second turn of charge moves - return this.end(); - } - - this.scene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger); - - if (this.move.moveId) { - this.showMoveText(); - } - - // This should only happen when there are no valid targets left on the field - if ((moveQueue.length && moveQueue[0].move === Moves.NONE) || !targets.length) { - this.showFailedText(); - this.cancel(); - - // Record a failed move so Abilities like Truant don't trigger next turn and soft-lock - this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); - - this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc. - - moveQueue.shift(); - return this.end(); - } - - if (!moveQueue.length || !moveQueue.shift()?.ignorePP) { // using .shift here clears out two turn moves once they've been used - this.move.usePp(ppUsed); - this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed)); - } - - if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) { - this.scene.currentBattle.lastMove = this.move.moveId; - } - - // Assume conditions affecting targets only apply to moves with a single target - let success = this.move.getMove().applyConditions(this.pokemon, targets[0], this.move.getMove()); - const cancelled = new Utils.BooleanHolder(false); - let failedText = this.move.getMove().getFailedText(this.pokemon, targets[0], this.move.getMove(), cancelled); - if (success && this.scene.arena.isMoveWeatherCancelled(this.move.getMove())) { - success = false; - } else if (success && this.scene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, this.move.getMove())) { - success = false; - if (failedText === null) { - failedText = getTerrainBlockMessage(targets[0], this.scene.arena.terrain?.terrainType!); // TODO: is this bang correct? - } - } - - /** - * Trigger pokemon type change before playing the move animation - * Will still change the user's type when using Roar, Whirlwind, Trick-or-Treat, and Forest's Curse, - * regardless of whether the move successfully executes or not. - */ - if (success || [Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) { - applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); - } - - if (success) { - this.scene.unshiftPhase(this.getEffectPhase()); - } else { - this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual }); - if (!cancelled.value) { - this.showFailedText(failedText); - } - } - // Checks if Dancer ability is triggered - if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !this.followUp) { - // Pokemon with Dancer can be on either side of the battle so we check in both cases - this.scene.getPlayerField().forEach(pokemon => { - applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets); - }); - this.scene.getEnemyField().forEach(pokemon => { - applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets); - }); - } - this.end(); - }; - - if (!this.followUp && this.pokemon.status && !this.pokemon.status.isPostTurn()) { - this.pokemon.status.incrementTurn(); - let activated = false; - let healed = false; - - switch (this.pokemon.status.effect) { - case StatusEffect.PARALYSIS: - if (!this.pokemon.randSeedInt(4, undefined, "Paralysis chance")) { - activated = true; - this.cancelled = true; - } - break; - case StatusEffect.SLEEP: - applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove()); - healed = this.pokemon.status.turnCount === this.pokemon.status.cureTurn; - activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP); - this.cancelled = activated; - break; - case StatusEffect.FREEZE: - healed = !!this.move.getMove().findAttr(attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE)) || !this.pokemon.randSeedInt(5, undefined, "Chance to thaw out from freeze"); - activated = !healed; - this.cancelled = activated; - break; - } - - if (activated) { - this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); - this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1))); - doMove(); - } else { - if (healed) { - this.scene.queueMessage(getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); - this.pokemon.resetStatus(); - this.pokemon.updateInfo(); - } - doMove(); - } - } else { - doMove(); - } - } - - getEffectPhase(): MoveEffectPhase { - return new MoveEffectPhase(this.scene, this.pokemon.getBattlerIndex(), this.targets, this.move); - } - - showMoveText(): void { - if (this.move.getMove().hasAttr(ChargeAttr)) { - const lastMove = this.pokemon.getLastXMoves() as TurnMove[]; - if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) { - this.scene.queueMessage(i18next.t("battle:useMove", { - pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), - moveName: this.move.getName() - }), 500); - return; - } - } - - if (this.pokemon.getTag(BattlerTagType.RECHARGING || BattlerTagType.INTERRUPTED)) { - return; - } - - this.scene.queueMessage(i18next.t("battle:useMove", { - pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), - moveName: this.move.getName() - }), 500); - applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents().find(() => true)!, this.move.getMove()); //TODO: is the bang correct here? - } - - showFailedText(failedText: string | null = null): void { - this.scene.queueMessage(failedText || i18next.t("battle:attackFailed")); - } - - end() { - if (!this.followUp && this.canMove()) { - this.scene.unshiftPhase(new MoveEndPhase(this.scene, this.pokemon.getBattlerIndex())); - } - - super.end(); - } -} -//#endregion - - - - - -//#region 41 MoveEffectPhase -export class MoveEffectPhase extends PokemonPhase { - public move: PokemonMove; - protected targets: BattlerIndex[]; - - constructor(scene: BattleScene, battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) { - super(scene, battlerIndex); - this.move = move; - /** - * In double battles, if the right Pokemon selects a spread move and the left Pokemon dies - * with no party members available to switch in, then the right Pokemon takes the index - * of the left Pokemon and gets hit unless this is checked. - */ - if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) { - const i = targets.indexOf(battlerIndex); - targets.splice(i, i + 1); - } - this.targets = targets; - } - - start() { - super.start(); - - /** The Pokemon using this phase's invoked move */ - const user = this.getUserPokemon(); - /** All Pokemon targeted by this phase's invoked move */ - const targets = this.getTargets(); - - /** If the user was somehow removed from the field, end this phase */ - if (!user?.isOnField()) { - return super.end(); - } - - /** - * Does an effect from this move override other effects on this turn? - * e.g. Charging moves (Fly, etc.) on their first turn of use. - */ - const overridden = new Utils.BooleanHolder(false); - /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ - const move = this.move.getMove(); - - // Assume single target for override - applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => { - // If other effects were overriden, stop this phase before they can be applied - if (overridden.value) { - return this.end(); - } - - user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); - - /** - * If this phase is for the first hit of the invoked move, - * resolve the move's total hit count. This block combines the - * effects of the move itself, Parental Bond, and Multi-Lens to do so. - */ - if (user.turnData.hitsLeft === undefined) { - const hitCount = new Utils.IntegerHolder(1); - // Assume single target for multi hit - applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount); - // If Parental Bond is applicable, double the hit count - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, targets.length, hitCount, new Utils.IntegerHolder(0)); - // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user - if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { - this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0)); - } - // Set the user's relevant turnData fields to reflect the final hit count - user.turnData.hitCount = hitCount.value; - user.turnData.hitsLeft = hitCount.value; - } - - /** - * Log to be entered into the user's move history once the move result is resolved. - * Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully - * used in the sense of "Does it have an effect on the user?". - */ - const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual }; - - /** - * Stores results of hit checks of the invoked move against all targets, organized by battler index. - * @see {@linkcode hitCheck} - */ - const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)])); - const hasActiveTargets = targets.some(t => t.isActive(true)); - /** - * If no targets are left for the move to hit (FAIL), or the invoked move is single-target - * (and not random target) and failed the hit check against its target (MISS), log the move - * as FAILed or MISSed (depending on the conditions above) and end this phase. - */ - if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]])) { - this.stopMultiHit(); - if (hasActiveTargets) { - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" })); - moveHistoryEntry.result = MoveResult.MISS; - applyMoveAttrs(MissEffectAttr, user, null, move); - } else { - this.scene.queueMessage(i18next.t("battle:attackFailed")); - moveHistoryEntry.result = MoveResult.FAIL; - } - user.pushMoveHistory(moveHistoryEntry); - return this.end(); - } - - /** All move effect attributes are chained together in this array to be applied asynchronously. */ - const applyAttrs: Promise[] = []; - - // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, () => { // TODO: is the bang correct here? - /** Has the move successfully hit a target (for damage) yet? */ - let hasHit: boolean = false; - for (const target of targets) { - /** - * If the move missed a target, stop all future hits against that target - * and move on to the next target (if there is one). - */ - if (!targetHitChecks[target.getBattlerIndex()]) { - this.stopMultiHit(target); - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); - if (moveHistoryEntry.result === MoveResult.PENDING) { - moveHistoryEntry.result = MoveResult.MISS; - } - user.pushMoveHistory(moveHistoryEntry); - applyMoveAttrs(MissEffectAttr, user, null, move); - continue; - } - - /** The {@linkcode ArenaTagSide} to which the target belongs */ - const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ - const hasConditionalProtectApplied = new Utils.BooleanHolder(false); - /** Does the applied conditional protection bypass Protect-ignoring effects? */ - const bypassIgnoreProtect = new Utils.BooleanHolder(false); - // If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects - if (!this.move.getMove().isAllyTarget()) { - this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); - } - - /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ - const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) - && (hasConditionalProtectApplied.value || target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))); - - /** Does this phase represent the invoked move's first strike? */ - const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); - - // Only log the move's result on the first strike - if (firstHit) { - user.pushMoveHistory(moveHistoryEntry); - } - - /** - * Since all fail/miss checks have applied, the move is considered successfully applied. - * It's worth noting that if the move has no effect or is protected against, this assignment - * is overwritten and the move is logged as a FAIL. - */ - moveHistoryEntry.result = MoveResult.SUCCESS; - - /** - * Stores the result of applying the invoked move to the target. - * If the target is protected, the result is always `NO_EFFECT`. - * Otherwise, the hit result is based on type effectiveness, immunities, - * and other factors that may negate the attack or status application. - * - * Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated - * (for attack moves) and the target's HP is updated. However, this isn't - * made visible to the user until the resulting {@linkcode DamagePhase} - * is invoked. - */ - const hitResult = !isProtected ? target.apply(user, move).hitResult : HitResult.NO_EFFECT; - - /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ - const dealsDamage = [ - HitResult.EFFECTIVE, - HitResult.SUPER_EFFECTIVE, - HitResult.NOT_VERY_EFFECTIVE, - HitResult.ONE_HIT_KO - ].includes(hitResult); - - /** Is this target the first one hit by the move on its current strike? */ - const firstTarget = dealsDamage && !hasHit; - if (firstTarget) { - hasHit = true; - } - - /** - * If the move has no effect on the target (i.e. the target is protected or immune), - * change the logged move result to FAIL. - */ - if (hitResult === HitResult.NO_EFFECT) { - moveHistoryEntry.result = MoveResult.FAIL; - } - - /** Does this phase represent the invoked move's last strike? */ - const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); - - /** - * If the user can change forms by using the invoked move, - * it only changes forms after the move's last hit - * (see Relic Song's interaction with Parental Bond when used by Meloetta). - */ - if (lastHit) { - this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); - } - - /** - * Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs. - * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger - * type requires different conditions to be met with respect to the move's hit result. - */ - applyAttrs.push(new Promise(resolve => { - // Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, - user, target, move).then(() => { - // All other effects require the move to not have failed or have been cancelled to trigger - if (hitResult !== HitResult.FAIL) { - /** Are the move's effects tied to the first turn of a charge move? */ - const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget() ?? null, move)); - /** - * If the invoked move's effects are meant to trigger during the move's "charge turn," - * ignore all effects after this point. - * Otherwise, apply all self-targeted POST_APPLY effects. - */ - Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY - && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => { - // All effects past this point require the move to have hit the target - if (hitResult !== HitResult.NO_EFFECT) { - // Apply all non-self-targeted POST_APPLY effects - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY - && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => { - /** - * If the move hit, and the target doesn't have Shield Dust, - * apply the chance to flinch the target gained from King's Rock - */ - if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) { - const flinched = new Utils.BooleanHolder(false); - user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); - if (flinched.value) { - target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); - } - } - // If the move was not protected against, apply all HIT effects - Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT - && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { - // Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them) - return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { - // If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tags and tokens - target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING); - if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) { - target.lapseTag(BattlerTagType.SHELL_TRAP); - } - if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { - user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); - } - })).then(() => { - // Apply the user's post-attack ability effects - applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { - /** - * If the invoked move is an attack, apply the user's chance to - * steal an item from the target granted by Grip Claw - */ - if (this.move.getMove() instanceof AttackMove) { - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); - } - resolve(); - }); - }); - }) - ).then(() => resolve()); - }); - } else { - applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve()); - } - }); - } else { - resolve(); - } - }); - })); - } - // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved - const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ? - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : - null; - - if (!!postTarget) { - if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after - applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); - } else { // Otherwise, push a new asynchronous move effect - applyAttrs.push(postTarget); - } - } - - // Wait for all move effects to finish applying, then end this phase - Promise.allSettled(applyAttrs).then(() => this.end()); - }); - }); - } - - end() { - const move = this.move.getMove(); - move.type = move.defaultType; - const user = this.getUserPokemon(); - /** - * If this phase isn't for the invoked move's last strike, - * unshift another MoveEffectPhase for the next strike. - * Otherwise, queue a message indicating the number of times the move has struck - * (if the move has struck more than once), then apply the heal from Shell Bell - * to the user. - */ - if (user) { - if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { - this.scene.unshiftPhase(this.getNewHitPhase()); - } else { - // Queue message for number of hits made by multi-move - // If multi-hit attack only hits once, still want to render a message - const hitsTotal = user.turnData.hitCount! - Math.max(user.turnData.hitsLeft!, 0); // TODO: are those bangs correct? - if (hitsTotal > 1 || (user.turnData.hitsLeft && user.turnData.hitsLeft > 0)) { - // If there are multiple hits, or if there are hits of the multi-hit move left - this.scene.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal })); - } - this.scene.applyModifiers(HitHealModifier, this.player, user); - } - } - - super.end(); - } - - /** - * Resolves whether this phase's invoked move hits or misses the given target - * @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move - * @returns `true` if the move does not miss the target; `false` otherwise - */ - hitCheck(target: Pokemon): boolean { - // Moves targeting the user and entry hazards can't miss - if ([MoveTarget.USER, MoveTarget.ENEMY_SIDE].includes(this.move.getMove().moveTarget)) { - return true; - } - - const user = this.getUserPokemon()!; // TODO: is this bang correct? - - // Hit check only calculated on first hit for multi-hit moves unless flag is set to check all hits. - // However, if an ability with the MaxMultiHitAbAttr, namely Skill Link, is present, act as a normal - // multi-hit move and proceed with all hits - if (user.turnData.hitsLeft < user.turnData.hitCount) { - if (!this.move.getMove().hasFlag(MoveFlags.CHECK_ALL_HITS) || user.hasAbilityWithAttr(MaxMultiHitAbAttr)) { - return true; - } - } - - if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { - return true; - } - - // 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().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { - return true; - } - - if (target.getTag(BattlerTagType.ALWAYS_GET_HIT)) { - return true; - } - - const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); - if (semiInvulnerableTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) { - return false; - } - - const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user!, target); // TODO: is the bang correct here? - - if (moveAccuracy === -1) { - return true; - } - - const accuracyMultiplier = user.getAccuracyMultiplier(target, this.move.getMove()); - const rand = user.randSeedInt(100, 1, "Accuracy roll"); - - return rand <= moveAccuracy * (accuracyMultiplier!); // TODO: is this bang correct? - } - - /** Returns the {@linkcode Pokemon} using this phase's invoked move */ - getUserPokemon(): Pokemon | undefined { - if (this.battlerIndex > BattlerIndex.ENEMY_2) { - return this.scene.getPokemonById(this.battlerIndex) ?? undefined; - } - return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; - } - - /** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */ - getTargets(): Pokemon[] { - return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); - } - - /** Returns the first target of this phase's invoked move */ - getTarget(): Pokemon | undefined { - return this.getTargets()[0]; - } - - /** - * Removes the given {@linkcode Pokemon} from this phase's target list - * @param target {@linkcode Pokemon} the Pokemon to be removed - */ - removeTarget(target: Pokemon): void { - const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); - if (targetIndex !== -1) { - this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1); - } - } - - /** - * Prevents subsequent strikes of this phase's invoked move from occurring - * @param target {@linkcode Pokemon} if defined, only stop subsequent - * strikes against this Pokemon - */ - stopMultiHit(target?: Pokemon): void { - /** If given a specific target, remove the target from subsequent strikes */ - if (target) { - this.removeTarget(target); - } - /** - * If no target specified, or the specified target was the last of this move's - * targets, completely cancel all subsequent strikes. - */ - if (!target || this.targets.length === 0 ) { - this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? - this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here? - } - } - - /** Returns a new MoveEffectPhase with the same properties as this phase */ - getNewHitPhase() { - return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); - } -} -//#endregion - - - - - -//#region 42 MoveEndPhase -export class MoveEndPhase extends PokemonPhase { - constructor(scene: BattleScene, battlerIndex: BattlerIndex) { - super(scene, battlerIndex); - } - - start() { - super.start(); - - const pokemon = this.getPokemon(); - if (pokemon.isActive(true)) { - pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); - } - - this.scene.arena.setIgnoreAbilities(false); - - this.end(); - } -} -//#endregion - - - - - -//#region 43 MoveAnimTestPhase -export class MoveAnimTestPhase extends BattlePhase { - private moveQueue: Moves[]; - - constructor(scene: BattleScene, moveQueue?: Moves[]) { - super(scene); - - this.moveQueue = moveQueue || Utils.getEnumValues(Moves).slice(1); - } - - start() { - const moveQueue = this.moveQueue.slice(0); - this.playMoveAnim(moveQueue, true); - } - - playMoveAnim(moveQueue: Moves[], player: boolean) { - const moveId = player ? moveQueue[0] : moveQueue.shift(); - if (moveId === undefined) { - this.playMoveAnim(this.moveQueue.slice(0), true); - return; - } else if (player) { - console.log(Moves[moveId]); - } - - initMoveAnim(this.scene, moveId).then(() => { - loadMoveAnimAssets(this.scene, [moveId], true) - .then(() => { - new MoveAnim(moveId, player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!, (player !== (allMoves[moveId] instanceof SelfStatusMove) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!).getBattlerIndex()).play(this.scene, () => { // TODO: are the bangs correct here? - if (player) { - this.playMoveAnim(moveQueue, false); - } else { - this.playMoveAnim(moveQueue, true); - } - }); - }); - }); - } -} -//#endregion - - - - - -//#region 44 ShowAbilityPhase -export class ShowAbilityPhase extends PokemonPhase { - private passive: boolean; - - constructor(scene: BattleScene, battlerIndex: BattlerIndex, passive: boolean = false) { - super(scene, battlerIndex); - - this.passive = passive; - } - - start() { - super.start(); - - const pokemon = this.getPokemon(); - - if (pokemon) { - this.scene.abilityBar.showAbility(pokemon, this.passive); - - if (pokemon?.battleData) { - pokemon.battleData.abilityRevealed = true; - } - } - - this.end(); - } -} -//#endregion - -export type StatChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void; - - - - -//#region 45 StatChangePhase -export class StatChangePhase extends PokemonPhase { - private stats: BattleStat[]; - private selfTarget: boolean; - private levels: integer; - private showMessage: boolean; - private ignoreAbilities: boolean; - private canBeCopied: boolean; - private onChange: StatChangeCallback | null; - - - constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatChangeCallback | null = null) { - super(scene, battlerIndex); - - this.selfTarget = selfTarget; - this.stats = stats; - this.levels = levels; - this.showMessage = showMessage; - this.ignoreAbilities = ignoreAbilities; - this.canBeCopied = canBeCopied; - this.onChange = onChange; - } - - start() { - const pokemon = this.getPokemon(); - - let random = false; - - if (this.stats.length === 1 && this.stats[0] === BattleStat.RAND) { - this.stats[0] = this.getRandomStat(); - random = true; - } - - this.aggregateStatChanges(random); - - if (!pokemon.isActive(true)) { - return this.end(); - } - - const filteredStats = this.stats.map(s => s !== BattleStat.RAND ? s : this.getRandomStat()).filter(stat => { - const cancelled = new Utils.BooleanHolder(false); - - if (!this.selfTarget && this.levels < 0) { - this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); - } - - if (!cancelled.value && !this.selfTarget && this.levels < 0) { - applyPreStatChangeAbAttrs(ProtectStatAbAttr, this.getPokemon(), stat, cancelled); - } - - return !cancelled.value; - }); - - const levels = new Utils.IntegerHolder(this.levels); - - if (!this.ignoreAbilities) { - applyAbAttrs(StatChangeMultiplierAbAttr, pokemon, null, levels); - } - - const battleStats = this.getPokemon().summonData.battleStats; - const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats![stat] + levels.value, 6) : Math.max(battleStats![stat] + levels.value, -6)) - battleStats![stat]); - - this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels); - - const end = () => { - if (this.showMessage) { - const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels); - for (const message of messages) { - this.scene.queueMessage(message); - } - } - - for (const stat of filteredStats) { - pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6); - } - - if (levels.value > 0 && this.canBeCopied) { - for (const opponent of pokemon.getOpponents()) { - applyAbAttrs(StatChangeCopyAbAttr, opponent, null, this.stats, levels.value); - } - } - - applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget); - - // Look for any other stat change phases; if this is the last one, do White Herb check - const existingPhase = this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex); - if (!(existingPhase instanceof StatChangePhase)) { - // Apply White Herb if needed - const whiteHerb = this.scene.applyModifier(PokemonResetNegativeStatStageModifier, this.player, pokemon) as PokemonResetNegativeStatStageModifier; - // If the White Herb was applied, consume it - if (whiteHerb) { - --whiteHerb.stackCount; - if (whiteHerb.stackCount <= 0) { - this.scene.removeModifier(whiteHerb); - } - this.scene.updateModifiers(this.player); - } - } - - pokemon.updateInfo(); - - handleTutorial(this.scene, Tutorial.Stat_Change).then(() => super.end()); - }; - - if (relLevels.filter(l => l).length && this.scene.moveAnimations) { - pokemon.enableMask(); - const pokemonMaskSprite = pokemon.maskSprite; - - const tileX = (this.player ? 106 : 236) * pokemon.getSpriteScale() * this.scene.field.scale; - const tileY = ((this.player ? 148 : 84) + (levels.value >= 1 ? 160 : 0)) * pokemon.getSpriteScale() * this.scene.field.scale; - const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale(); - const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale(); - - // On increase, show the red sprite located at ATK - // On decrease, show the blue sprite located at SPD - const spriteColor = levels.value >= 1 ? BattleStat[BattleStat.ATK].toLowerCase() : BattleStat[BattleStat.SPD].toLowerCase(); - const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor); - statSprite.setPipeline(this.scene.fieldSpritePipeline); - statSprite.setAlpha(0); - statSprite.setScale(6); - statSprite.setOrigin(0.5, 1); - - this.scene.playSound(`stat_${levels.value >= 1 ? "up" : "down"}`); - - statSprite.setMask(new Phaser.Display.Masks.BitmapMask(this.scene, pokemonMaskSprite ?? undefined)); - - this.scene.tweens.add({ - targets: statSprite, - duration: 250, - alpha: 0.8375, - onComplete: () => { - this.scene.tweens.add({ - targets: statSprite, - delay: 1000, - duration: 250, - alpha: 0 - }); - } - }); - - this.scene.tweens.add({ - targets: statSprite, - duration: 1500, - y: `${levels.value >= 1 ? "-" : "+"}=${160 * 6}` - }); - - this.scene.time.delayedCall(1750, () => { - pokemon.disableMask(); - end(); - }); - } else { - end(); - } - } - - getRandomStat(): BattleStat { - const allStats = Utils.getEnumValues(BattleStat); - return this.getPokemon() ? allStats[this.getPokemon()!.randSeedInt(BattleStat.SPD + 1, undefined, "Randomly selecting a stat")] : BattleStat.ATK; // TODO: return default ATK on random? idk... - } - - aggregateStatChanges(random: boolean = false): void { - const isAccEva = [BattleStat.ACC, BattleStat.EVA].some(s => this.stats.includes(s)); - let existingPhase: StatChangePhase; - if (this.stats.length === 1) { - while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1 - && (p.stats[0] === this.stats[0] || (random && p.stats[0] === BattleStat.RAND)) - && p.selfTarget === this.selfTarget && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) { - if (existingPhase.stats[0] === BattleStat.RAND) { - existingPhase.stats[0] = this.getRandomStat(); - if (existingPhase.stats[0] !== this.stats[0]) { - continue; - } - } - this.levels += existingPhase.levels; - - if (!this.scene.tryRemovePhase(p => p === existingPhase)) { - break; - } - } - } - while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget - && ([BattleStat.ACC, BattleStat.EVA].some(s => p.stats.includes(s)) === isAccEva) - && p.levels === this.levels && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) { - this.stats.push(...existingPhase.stats); - if (!this.scene.tryRemovePhase(p => p === existingPhase)) { - break; - } - } - } - - getStatChangeMessages(stats: BattleStat[], levels: integer, relLevels: integer[]): string[] { - const messages: string[] = []; - - const relLevelStatIndexes = {}; - for (let rl = 0; rl < relLevels.length; rl++) { - const relLevel = relLevels[rl]; - if (!relLevelStatIndexes[relLevel]) { - relLevelStatIndexes[relLevel] = []; - } - relLevelStatIndexes[relLevel].push(rl); - } - - Object.keys(relLevelStatIndexes).forEach(rl => { - const relLevelStats = stats.filter((_, i) => relLevelStatIndexes[rl].includes(i)); - let statsFragment = ""; - - if (relLevelStats.length > 1) { - statsFragment = relLevelStats.length >= 5 - ? i18next.t("battle:stats") - : `${relLevelStats.slice(0, -1).map(s => getBattleStatName(s)).join(", ")}${relLevelStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${getBattleStatName(relLevelStats[relLevelStats.length - 1])}`; - messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1,relLevelStats.length)); - } else { - statsFragment = getBattleStatName(relLevelStats[0]); - messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1,relLevelStats.length)); - } - }); - - return messages; - } -} -//#endregion - - - - - -//#region 46 WeatherEffectPhase -export class WeatherEffectPhase extends CommonAnimPhase { - public weather: Weather | null; - - constructor(scene: BattleScene) { - super(scene, undefined, undefined, CommonAnim.SUNNY + ((scene?.arena?.weather?.weatherType || WeatherType.NONE) - 1)); - this.weather = scene?.arena?.weather; - } - - start() { - // Update weather state with any changes that occurred during the turn - this.weather = this.scene?.arena?.weather; - - if (!this.weather) { - this.end(); - return; - } - - this.setAnimation(CommonAnim.SUNNY + (this.weather.weatherType - 1)); - - if (this.weather.isDamaging()) { - - const cancelled = new Utils.BooleanHolder(false); - - this.executeForAll((pokemon: Pokemon) => applyPreWeatherEffectAbAttrs(SuppressWeatherEffectAbAttr, pokemon, this.weather, cancelled)); - - if (!cancelled.value) { - const inflictDamage = (pokemon: Pokemon) => { - const cancelled = new Utils.BooleanHolder(false); - - applyPreWeatherEffectAbAttrs(PreWeatherDamageAbAttr, pokemon, this.weather , cancelled); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); - - if (cancelled.value) { - return; - } - - const damage = Math.ceil(pokemon.getMaxHp() / 16); - - this.scene.queueMessage(getWeatherDamageMessage(this.weather?.weatherType!, pokemon)!); // TODO: are those bangs correct? - pokemon.damageAndUpdate(damage, HitResult.EFFECTIVE, false, false, true); - }; - - this.executeForAll((pokemon: Pokemon) => { - const immune = !pokemon || !!pokemon.getTypes(true, true).filter(t => this.weather?.isTypeDamageImmune(t)).length; - if (!immune) { - inflictDamage(pokemon); - } - }); - } - } - - this.scene.ui.showText(getWeatherLapseMessage(this.weather.weatherType)!, null, () => { // TODO: is this bang correct? - this.executeForAll((pokemon: Pokemon) => applyPostWeatherLapseAbAttrs(PostWeatherLapseAbAttr, pokemon, this.weather)); - - super.start(); - }); - } -} -//#endregion - - - - - -//#region 47 ObtainStatusEffectPhase -export class ObtainStatusEffectPhase extends PokemonPhase { - private statusEffect: StatusEffect | undefined; - private cureTurn: integer | null; - private sourceText: string | null; - private sourcePokemon: Pokemon | null; - - constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string, sourcePokemon?: Pokemon) { - super(scene, battlerIndex); - - this.statusEffect = statusEffect; - this.cureTurn = cureTurn!; // TODO: is this bang correct? - this.sourceText = sourceText!; // TODO: is this bang correct? - this.sourcePokemon = sourcePokemon!; // For tracking which Pokemon caused the status effect // TODO: is this bang correct? - } - - start() { - const pokemon = this.getPokemon(); - if (!pokemon?.status) { - if (pokemon?.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { - if (this.cureTurn) { - pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct? - } - pokemon.updateInfo(true); - new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, () => { - this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined)); - if (pokemon.status?.isPostTurn()) { - this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex)); - } - this.end(); - }); - return; - } - } else if (pokemon.status.effect === this.statusEffect) { - this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon))); - } - this.end(); - } -} -//#endregion - - - - - -//#region 48 PostTurnStatusEffectPhase -export class PostTurnStatusEffectPhase extends PokemonPhase { - constructor(scene: BattleScene, battlerIndex: BattlerIndex) { - super(scene, battlerIndex); - } - - start() { - const pokemon = this.getPokemon(); - if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn()) { - pokemon.status.incrementTurn(); - const cancelled = new Utils.BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); - applyAbAttrs(BlockStatusDamageAbAttr, pokemon, cancelled); - - if (!cancelled.value) { - this.scene.queueMessage(getStatusEffectActivationText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); - const damage = new Utils.NumberHolder(0); - switch (pokemon.status.effect) { - case StatusEffect.POISON: - damage.value = Math.max(pokemon.getMaxHp() >> 3, 1); - break; - case StatusEffect.TOXIC: - damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.turnCount), 1); - break; - case StatusEffect.BURN: - damage.value = Math.max(pokemon.getMaxHp() >> 4, 1); - applyAbAttrs(ReduceBurnDamageAbAttr, pokemon, null, damage); - break; - } - if (damage.value) { - // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... - this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); - pokemon.updateInfo(); - } - new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, () => this.end()); - } else { - this.end(); - } - } else { - this.end(); - } - } - - override end() { - if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { - this.scene.initFinalBossPhaseTwo(this.getPokemon()); - } else { - super.end(); - } - } -} -//#endregion - - - - - -//#region 49 MessagePhase -export class MessagePhase extends Phase { - private text: string; - private callbackDelay: integer | null; - private prompt: boolean | null; - private promptDelay: integer | null; - - constructor(scene: BattleScene, text: string, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) { - super(scene); - - this.text = text; - this.callbackDelay = callbackDelay!; // TODO: is this bang correct? - this.prompt = prompt!; // TODO: is this bang correct? - this.promptDelay = promptDelay!; // TODO: is this bang correct? - } - - start() { - super.start(); - - if (this.text.indexOf("$") > -1) { - const pageIndex = this.text.indexOf("$"); - this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay)); - this.text = this.text.slice(0, pageIndex).trim(); - } - - this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay); - } - - end() { - if (this.scene.abilityBar.shown) { - this.scene.abilityBar.hide(); - } - - super.end(); - } -} -//#endregion - - - - - -//#region 50 DamagePhase -export class DamagePhase extends PokemonPhase { - private amount: integer; - private damageResult: DamageResult; - private critical: boolean; - - constructor(scene: BattleScene, battlerIndex: BattlerIndex, amount: integer, damageResult?: DamageResult, critical: boolean = false) { - super(scene, battlerIndex); - - this.amount = amount; - this.damageResult = damageResult || HitResult.EFFECTIVE; - this.critical = critical; - } - - start() { - super.start(); - - if (this.damageResult === HitResult.ONE_HIT_KO) { - if (this.scene.moveAnimations) { - this.scene.toggleInvert(true); - } - this.scene.time.delayedCall(Utils.fixedInt(1000), () => { - this.scene.toggleInvert(false); - this.applyDamage(); - }); - return; - } - - this.applyDamage(); - } - - updateAmount(amount: integer): void { - this.amount = amount; - } - - applyDamage() { - switch (this.damageResult) { - case HitResult.EFFECTIVE: - this.scene.playSound("hit"); - break; - case HitResult.SUPER_EFFECTIVE: - case HitResult.ONE_HIT_KO: - this.scene.playSound("hit_strong"); - break; - case HitResult.NOT_VERY_EFFECTIVE: - this.scene.playSound("hit_weak"); - break; - } - - if (this.amount) { - this.scene.damageNumberHandler.add(this.getPokemon(), this.amount, this.damageResult, this.critical); - } - - if (this.damageResult !== HitResult.OTHER) { - const flashTimer = this.scene.time.addEvent({ - delay: 100, - repeat: 5, - startAt: 200, - callback: () => { - this.getPokemon().getSprite().setVisible(flashTimer.repeatCount % 2 === 0); - if (!flashTimer.repeatCount) { - this.getPokemon().updateInfo().then(() => this.end()); - } - } - }); - } else { - this.getPokemon().updateInfo().then(() => this.end()); - } - } - - end() { - switch (this.scene.currentBattle.battleSpec) { - case BattleSpec.FINAL_BOSS: - const pokemon = this.getPokemon(); - if (pokemon instanceof EnemyPokemon && pokemon.isBoss() && !pokemon.formIndex && pokemon.bossSegmentIndex < 1) { - this.scene.fadeOutBgm(Utils.fixedInt(2000), false); - this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].firstStageWin, pokemon.species.name, null, () => { - this.scene.addEnemyModifier(getModifierType(modifierTypes.MINI_BLACK_HOLE).newModifier(pokemon) as PersistentModifier, false, true); - pokemon.generateAndPopulateMoveset(1); - this.scene.setFieldScale(0.75); - this.scene.initFinalBossPhaseTwo(this.getPokemon()); - this.scene.currentBattle.double = true; - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); - if (availablePartyMembers.length > 1) { - this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, true)); - if (!availablePartyMembers[1].isOnField()) { - this.scene.pushPhase(new SummonPhase(this.scene, 1)); - } - } - - super.end(); - }); - return; - } - break; - } - - super.end(); - } -} -//#endregion - - - - - -//#region 51 FaintPhase -export class FaintPhase extends PokemonPhase { - private preventEndure: boolean; - - constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure?: boolean) { - super(scene, battlerIndex); - - this.preventEndure = preventEndure!; // TODO: is this bang correct? - } - - start() { - super.start(); - - if (!this.preventEndure) { - const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier; - - if (instantReviveModifier) { - if (!--instantReviveModifier.stackCount) { - this.scene.removeModifier(instantReviveModifier); - } - this.scene.updateModifiers(this.player); - return this.end(); - } - } - - if (!this.tryOverrideForBattleSpec()) { - this.doFaint(); - } - } - - doFaint(): void { - const pokemon = this.getPokemon(); - - - // Track total times pokemon have been KO'd for supreme overlord/last respects - if (pokemon.isPlayer()) { - this.scene.currentBattle.playerFaints += 1; - } else { - this.scene.currentBattle.enemyFaints += 1; - } - - this.scene.queueMessage(i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), null, true); - - if (pokemon.turnData?.attacksReceived?.length) { - const lastAttack = pokemon.turnData.attacksReceived[0]; - applyPostFaintAbAttrs(PostFaintAbAttr, pokemon, this.scene.getPokemonById(lastAttack.sourceId)!, new PokemonMove(lastAttack.move).getMove(), lastAttack.result); // TODO: is this bang correct? - } - - const alivePlayField = this.scene.getField(true); - alivePlayField.forEach(p => applyPostKnockOutAbAttrs(PostKnockOutAbAttr, p, pokemon)); - if (pokemon.turnData?.attacksReceived?.length) { - const defeatSource = this.scene.getPokemonById(pokemon.turnData.attacksReceived[0].sourceId); - if (defeatSource?.isOnField()) { - applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource); - const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move]; - const pvattrs = pvmove.getAttrs(PostVictoryStatChangeAttr); - if (pvattrs.length) { - for (const pvattr of pvattrs) { - pvattr.applyPostVictory(defeatSource, defeatSource, pvmove); - } - } - } - } - - if (this.player) { - /** The total number of Pokemon in the player's party that can legally fight */ - const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); - /** The total number of legal player Pokemon that aren't currently on the field */ - const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true)); - if (!legalPlayerPokemon.length) { - /** If the player doesn't have any legal Pokemon, end the game */ - this.scene.unshiftPhase(new GameOverPhase(this.scene)); - } else if (this.scene.currentBattle.double && legalPlayerPokemon.length === 1 && legalPlayerPartyPokemon.length === 0) { - /** - * If the player has exactly one Pokemon in total at this point in a double battle, and that Pokemon - * is already on the field, unshift a phase that moves that Pokemon to center position. - */ - this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); - } else if (legalPlayerPartyPokemon.length > 0) { - /** - * If previous conditions weren't met, and the player has at least 1 legal Pokemon off the field, - * push a phase that prompts the player to summon a Pokemon from their party. - */ - LoggerTools.isFaintSwitch.value = true; - this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false)); - } - } else { - this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); - if (this.scene.currentBattle.battleType === BattleType.TRAINER) { - const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length; - if (hasReservePartyMember) { - this.scene.pushPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, -1, false, false, false)); - } - } - } - - // in double battles redirect potential moves off fainted pokemon - if (this.scene.currentBattle.double) { - const allyPokemon = pokemon.getAlly(); - this.scene.redirectPokemonMoves(pokemon, allyPokemon); - } - - pokemon.lapseTags(BattlerTagLapseType.FAINT); - this.scene.getField(true).filter(p => p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id)); - - pokemon.faintCry(() => { - if (pokemon instanceof PlayerPokemon) { - pokemon.addFriendship(-10); - } - pokemon.hideInfo(); - this.scene.playSound("faint"); - this.scene.tweens.add({ - targets: pokemon, - duration: 500, - y: pokemon.y + 150, - ease: "Sine.easeIn", - onComplete: () => { - pokemon.setVisible(false); - pokemon.y -= 150; - pokemon.trySetStatus(StatusEffect.FAINT); - if (pokemon.isPlayer()) { - this.scene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon); - } else { - this.scene.addFaintedEnemyScore(pokemon as EnemyPokemon); - this.scene.currentBattle.addPostBattleLoot(pokemon as EnemyPokemon); - } - this.scene.field.remove(pokemon); - this.end(); - } - }); - }); - } - - tryOverrideForBattleSpec(): boolean { - switch (this.scene.currentBattle.battleSpec) { - case BattleSpec.FINAL_BOSS: - if (!this.player) { - const enemy = this.getPokemon(); - if (enemy.formIndex) { - this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].secondStageWin, enemy.species.name, null, () => this.doFaint()); - } else { - // Final boss' HP threshold has been bypassed; cancel faint and force check for 2nd phase - enemy.hp++; - this.scene.unshiftPhase(new DamagePhase(this.scene, enemy.getBattlerIndex(), 0, HitResult.OTHER)); - this.end(); - } - return true; - } - } - - return false; - } -} -//#endregion - - - - - -//#region 52 VictoryPhase -export class VictoryPhase extends PokemonPhase { - constructor(scene: BattleScene, battlerIndex: BattlerIndex) { - super(scene, battlerIndex); - } - - start() { - super.start(); - - this.scene.gameData.gameStats.pokemonDefeated++; - - const participantIds = this.scene.currentBattle.playerParticipantIds; - const party = this.scene.getParty(); - const expShareModifier = this.scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; - const expBalanceModifier = this.scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; - const multipleParticipantExpBonusModifier = this.scene.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; - const nonFaintedPartyMembers = party.filter(p => p.hp); - const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.scene.getMaxExpLevel()); - const partyMemberExp: number[] = []; - - if (participantIds.size) { - let expValue = this.getPokemon().getExpValue(); - if (this.scene.currentBattle.battleType === BattleType.TRAINER) { - expValue = Math.floor(expValue * 1.5); - } - for (const partyMember of nonFaintedPartyMembers) { - const pId = partyMember.id; - const participated = participantIds.has(pId); - if (participated) { - partyMember.addFriendship(2); - } - if (!expPartyMembers.includes(partyMember)) { - continue; - } - if (!participated && !expShareModifier) { - partyMemberExp.push(0); - continue; - } - let expMultiplier = 0; - if (participated) { - expMultiplier += (1 / participantIds.size); - if (participantIds.size > 1 && multipleParticipantExpBonusModifier) { - expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2; - } - } else if (expShareModifier) { - expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size; - } - if (partyMember.pokerus) { - expMultiplier *= 1.5; - } - if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) { - expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; - } - const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier); - this.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); - partyMemberExp.push(Math.floor(pokemonExp.value)); - } - - if (expBalanceModifier) { - let totalLevel = 0; - let totalExp = 0; - expPartyMembers.forEach((expPartyMember, epm) => { - totalExp += partyMemberExp[epm]; - totalLevel += expPartyMember.level; - }); - - const medianLevel = Math.floor(totalLevel / expPartyMembers.length); - - const recipientExpPartyMemberIndexes: number[] = []; - expPartyMembers.forEach((expPartyMember, epm) => { - if (expPartyMember.level <= medianLevel) { - recipientExpPartyMemberIndexes.push(epm); - } - }); - - const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length); - - expPartyMembers.forEach((_partyMember, pm) => { - partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount()); - }); - } - - for (let pm = 0; pm < expPartyMembers.length; pm++) { - const exp = partyMemberExp[pm]; - - if (exp) { - const partyMemberIndex = party.indexOf(expPartyMembers[pm]); - this.scene.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(this.scene, partyMemberIndex, exp) : new ShowPartyExpBarPhase(this.scene, partyMemberIndex, exp)); - } - } - } - - if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType ? !p?.isFainted(true) : p.isOnField())) { - this.scene.pushPhase(new BattleEndPhase(this.scene)); - if (this.scene.currentBattle.battleType === BattleType.TRAINER) { - this.scene.pushPhase(new TrainerVictoryPhase(this.scene)); - } - if (this.scene.gameMode.isEndless || !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) { - this.scene.pushPhase(new EggLapsePhase(this.scene)); - if (this.scene.currentBattle.waveIndex % 10) { - this.scene.pushPhase(new SelectModifierPhase(this.scene)); - } else if (this.scene.gameMode.isDaily) { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, "") - this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM)); - if (this.scene.currentBattle.waveIndex > 10 && !this.scene.gameMode.isWaveFinal(this.scene.currentBattle.waveIndex)) { - this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.GOLDEN_POKEBALL)); - } - } else { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, "") - const superExpWave = !this.scene.gameMode.isEndless ? (this.scene.offsetGym ? 0 : 20) : 10; - if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex === 10) { - this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_SHARE)); - } - if (this.scene.currentBattle.waveIndex <= 750 && (this.scene.currentBattle.waveIndex <= 500 || (this.scene.currentBattle.waveIndex % 30) === superExpWave)) { - this.scene.pushPhase(new ModifierRewardPhase(this.scene, (this.scene.currentBattle.waveIndex % 30) !== superExpWave || this.scene.currentBattle.waveIndex > 250 ? modifierTypes.EXP_CHARM : modifierTypes.SUPER_EXP_CHARM)); - } - if (this.scene.currentBattle.waveIndex <= 150 && !(this.scene.currentBattle.waveIndex % 50)) { - this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.GOLDEN_POKEBALL)); - } - if (this.scene.gameMode.isEndless && !(this.scene.currentBattle.waveIndex % 50)) { - this.scene.pushPhase(new ModifierRewardPhase(this.scene, !(this.scene.currentBattle.waveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS)); - this.scene.pushPhase(new AddEnemyBuffModifierPhase(this.scene)); - } - } - this.scene.pushPhase(new NewBattlePhase(this.scene)); - } else { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, "") - this.scene.currentBattle.battleType = BattleType.CLEAR; - this.scene.score += this.scene.gameMode.getClearScoreBonus(); - this.scene.updateScoreText(); - this.scene.pushPhase(new GameOverPhase(this.scene, true)); - } - } - - this.end(); - } -} -//#endregion - - - - - -//#region 53 TrainerVictoryPhase -export class TrainerVictoryPhase extends BattlePhase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - this.scene.disableMenu = true; - - this.scene.playBgm(this.scene.currentBattle.trainer?.config.victoryBgm); - - this.scene.unshiftPhase(new MoneyRewardPhase(this.scene, this.scene.currentBattle.trainer?.config.moneyMultiplier!)); // TODO: is this bang correct? - - const modifierRewardFuncs = this.scene.currentBattle.trainer?.config.modifierRewardFuncs!; // TODO: is this bang correct? - for (const modifierRewardFunc of modifierRewardFuncs) { - this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, modifierRewardFunc)); - } - - const trainerType = this.scene.currentBattle.trainer?.config.trainerType!; // TODO: is this bang correct? - if (vouchers.hasOwnProperty(TrainerType[trainerType])) { - if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer?.config.isBoss) { - this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][vouchers[TrainerType[trainerType]].voucherType])); - } - } - - this.scene.ui.showText(i18next.t("battle:trainerDefeated", { trainerName: this.scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }), null, () => { - const victoryMessages = this.scene.currentBattle.trainer?.getVictoryMessages()!; // TODO: is this bang correct? - let message: string; - this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(victoryMessages), this.scene.currentBattle.waveIndex); - message = message!; // tell TS compiler it's defined now - - const showMessage = () => { - const originalFunc = showMessageOrEnd; - showMessageOrEnd = () => this.scene.ui.showDialogue(message, this.scene.currentBattle.trainer?.getName(), null, originalFunc); - - showMessageOrEnd(); - }; - let showMessageOrEnd = () => this.end(); - if (victoryMessages?.length) { - if (this.scene.currentBattle.trainer?.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) { - const originalFunc = showMessageOrEnd; - showMessageOrEnd = () => this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => originalFunc())); - this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(this.scene.currentBattle.trainer?.getKey()!, getCharVariantFromDialogue(victoryMessages[0])).then(() => showMessage())); // TODO: is this bang correct? - } else { - showMessage(); - } - } else { - showMessageOrEnd(); - } - }, null, true); - - this.showEnemyTrainer(); - } -} -//#endregion - - - - - -//#region 54 MoneyRewardPhase -export class MoneyRewardPhase extends BattlePhase { - private moneyMultiplier: number; - - constructor(scene: BattleScene, moneyMultiplier: number) { - super(scene); - - this.moneyMultiplier = moneyMultiplier; - } - - start() { - const moneyAmount = new Utils.IntegerHolder(this.scene.getWaveMoneyAmount(this.moneyMultiplier)); - - this.scene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); - - if (this.scene.arena.getTag(ArenaTagType.HAPPY_HOUR)) { - moneyAmount.value *= 2; - } - - this.scene.addMoney(moneyAmount.value); - - const userLocale = navigator.language || "en-US"; - const formattedMoneyAmount = moneyAmount.value.toLocaleString(userLocale); - const message = i18next.t("battle:moneyWon", { moneyAmount: formattedMoneyAmount }); - - this.scene.ui.showText(message, null, () => this.end(), null, true); - } -} -//#endregion - - - - - -//#region 55 ModifierRewardPhase -export class ModifierRewardPhase extends BattlePhase { - protected modifierType: ModifierType; - - constructor(scene: BattleScene, modifierTypeFunc: ModifierTypeFunc) { - super(scene); - - this.modifierType = getModifierType(modifierTypeFunc); - } - - start() { - super.start(); - - this.doReward().then(() => this.end()); - } - - doReward(): Promise { - return new Promise(resolve => { - const newModifier = this.modifierType.newModifier(); - this.scene.addModifier(newModifier).then(() => { - this.scene.playSound("item_fanfare"); - this.scene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => resolve(), null, true); - }); - }); - } -} -//#endregion - - - - - -//#region 56 GameOverModifierRewardPhase -export class GameOverModifierRewardPhase extends ModifierRewardPhase { - constructor(scene: BattleScene, modifierTypeFunc: ModifierTypeFunc) { - super(scene, modifierTypeFunc); - } - - doReward(): Promise { - return new Promise(resolve => { - const newModifier = this.modifierType.newModifier(); - this.scene.addModifier(newModifier).then(() => { - this.scene.playSound("level_up_fanfare"); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.fadeIn(250).then(() => { - this.scene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => { - this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true)); - resolve(); - }, null, true, 1500); - }); - }); - }); - } -} -//#endregion - - - - - -//#region 57 RibbonModifierRewardPhase -export class RibbonModifierRewardPhase extends ModifierRewardPhase { - private species: PokemonSpecies; - - constructor(scene: BattleScene, modifierTypeFunc: ModifierTypeFunc, species: PokemonSpecies) { - super(scene, modifierTypeFunc); - - this.species = species; - } - - doReward(): Promise { - return new Promise(resolve => { - const newModifier = this.modifierType.newModifier(); - this.scene.addModifier(newModifier).then(() => { - this.scene.playSound("level_up_fanfare"); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t("battle:beatModeFirstTime", { - speciesName: this.species.name, - gameMode: this.scene.gameMode.getName(), - newModifier: newModifier?.type.name - }), null, () => { - resolve(); - }, null, true, 1500); - }); - }); - } -} -//#endregion - - - - - -//#region 58 GameOverPhase -export class GameOverPhase extends BattlePhase { - private victory: boolean; - private firstRibbons: PokemonSpecies[] = []; - - constructor(scene: BattleScene, victory?: boolean) { - super(scene); - - this.victory = !!victory; - } - - start() { - super.start(); - - // Failsafe if players somehow skip floor 200 in classic mode - if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex > 200) { - this.victory = true; - } - - if (this.victory && this.scene.gameMode.isEndless) { - this.scene.ui.showDialogue(i18next.t("PGMmiscDialogue:ending_endless"), i18next.t("PGMmiscDialogue:ending_name"), 0, () => this.handleGameOver()); - } else if (this.victory || !this.scene.enableRetries) { - this.handleGameOver(); - } else { - this.scene.ui.showText(i18next.t("battle:retryBattle"), null, () => { - this.scene.ui.setMode(Mode.CONFIRM, () => { - this.scene.ui.fadeOut(1250).then(() => { - this.scene.reset(); - this.scene.clearPhaseQueue(); - this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => { - this.scene.pushPhase(new EncounterPhase(this.scene, true)); - - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; - - this.scene.pushPhase(new SummonPhase(this.scene, 0)); - if (this.scene.currentBattle.double && availablePartyMembers > 1) { - this.scene.pushPhase(new SummonPhase(this.scene, 1)); - } - if (this.scene.currentBattle.waveIndex > 1 && this.scene.currentBattle.battleType !== BattleType.TRAINER) { - this.scene.pushPhase(new CheckSwitchPhase(this.scene, 0, this.scene.currentBattle.double)); - if (this.scene.currentBattle.double && availablePartyMembers > 1) { - this.scene.pushPhase(new CheckSwitchPhase(this.scene, 1, this.scene.currentBattle.double)); - } - } - - this.scene.ui.fadeIn(1250); - this.end(); - }); - }); - }, () => this.handleGameOver(), false, 0, 0, 1000); - }); - } - } - - handleGameOver(): void { - const doGameOver = (newClear: boolean) => { - this.scene.disableMenu = true; - this.scene.time.delayedCall(1000, () => { - let firstClear = false; - if (this.victory && newClear) { - if (this.scene.gameMode.isClassic) { - firstClear = this.scene.validateAchv(achvs.CLASSIC_VICTORY); - this.scene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY); - this.scene.gameData.gameStats.sessionsWon++; - for (const pokemon of this.scene.getParty()) { - this.awardRibbon(pokemon); - - if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) { - this.awardRibbon(pokemon, true); - } - } - } else if (this.scene.gameMode.isDaily && newClear) { - this.scene.gameData.gameStats.dailyRunSessionsWon++; - } - } - const fadeDuration = this.victory ? 10000 : 5000; - this.scene.fadeOutBgm(fadeDuration, true); - const activeBattlers = this.scene.getField().filter(p => p?.isActive(true)); - activeBattlers.map(p => p.hideInfo()); - this.scene.ui.fadeOut(fadeDuration).then(() => { - activeBattlers.map(a => a.setVisible(false)); - this.scene.setFieldScale(1, true); - this.scene.clearPhaseQueue(); - this.scene.ui.clearText(); - - if (this.victory && this.scene.gameMode.isChallenge) { - this.scene.gameMode.challenges.forEach(c => this.scene.validateAchvs(ChallengeAchv, c)); - } - - const clear = (endCardPhase?: EndCardPhase) => { - if (newClear) { - this.handleUnlocks(); - } - if (this.victory && newClear) { - for (const species of this.firstRibbons) { - this.scene.unshiftPhase(new RibbonModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PLUS, species)); - } - if (!firstClear) { - this.scene.unshiftPhase(new GameOverModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PREMIUM)); - } - } - this.scene.pushPhase(new PostGameOverPhase(this.scene, endCardPhase)); - this.end(); - }; - - if (this.victory && this.scene.gameMode.isClassic) { - const message = miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1]; - - if (!this.scene.ui.shouldSkipDialogue(message)) { - this.scene.ui.fadeIn(500).then(() => { - this.scene.charSprite.showCharacter(`rival_${this.scene.gameData.gender === PlayerGender.FEMALE ? "m" : "f"}`, getCharVariantFromDialogue(miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1])).then(() => { - this.scene.ui.showDialogue(message, this.scene.gameData.gender === PlayerGender.FEMALE ? trainerConfigs[TrainerType.RIVAL].name : trainerConfigs[TrainerType.RIVAL].nameFemale, null, () => { - this.scene.ui.fadeOut(500).then(() => { - this.scene.charSprite.hide().then(() => { - const endCardPhase = new EndCardPhase(this.scene); - this.scene.unshiftPhase(endCardPhase); - clear(endCardPhase); - }); - }); - }); - }); - }); - } else { - const endCardPhase = new EndCardPhase(this.scene); - this.scene.unshiftPhase(endCardPhase); - clear(endCardPhase); - } - } else { - clear(); - } - }); - }); - }; - - /* Added a local check to see if the game is running offline on victory - If Online, execute apiFetch as intended - If Offline, execute offlineNewClear(), a localStorage implementation of newClear daily run checks */ - if (this.victory) { - if (!Utils.isLocal) { - Utils.apiFetch(`savedata/session/newclear?slot=${this.scene.sessionSlotId}&clientSessionId=${clientSessionId}`, true) - .then(response => response.json()) - .then(newClear => doGameOver(newClear)); - } else { - this.scene.gameData.offlineNewClear(this.scene).then(result => { - doGameOver(result); - }); - } - } else { - doGameOver(false); - } - } - - handleUnlocks(): void { - if (this.victory && this.scene.gameMode.isClassic) { - if (!this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { - this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.ENDLESS_MODE)); - } - if (this.scene.getParty().filter(p => p.fusionSpecies).length && !this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { - this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.SPLICED_ENDLESS_MODE)); - } - if (!this.scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) { - this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.MINI_BLACK_HOLE)); - } - if (!this.scene.gameData.unlocks[Unlockables.EVIOLITE] && this.scene.getParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)) { - this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.EVIOLITE)); - } - } - } - - awardRibbon(pokemon: Pokemon, forStarter: boolean = false): void { - const speciesId = getPokemonSpecies(pokemon.species.speciesId); - const speciesRibbonCount = this.scene.gameData.incrementRibbonCount(speciesId, forStarter); - // first time classic win, award voucher - if (speciesRibbonCount === 1) { - this.firstRibbons.push(getPokemonSpecies(pokemon.species.getRootSpeciesId(forStarter))); - } - } -} -//#endregion - - - - - -//#region 59 EndCardPhase -export class EndCardPhase extends Phase { - public endCard: Phaser.GameObjects.Image; - public text: Phaser.GameObjects.Text; - - constructor(scene: BattleScene) { - super(scene); - } - - start(): void { - super.start(); - - this.scene.ui.getMessageHandler().bg.setVisible(false); - this.scene.ui.getMessageHandler().nameBoxContainer.setVisible(false); - - this.endCard = this.scene.add.image(0, 0, `end_${this.scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}`); - this.endCard.setOrigin(0); - this.endCard.setScale(0.5); - this.scene.field.add(this.endCard); - - this.text = addTextObject(this.scene, this.scene.game.canvas.width / 12, (this.scene.game.canvas.height / 6) - 16, i18next.t("battle:congratulations"), TextStyle.SUMMARY, { fontSize: "128px" }); - this.text.setOrigin(0.5); - this.scene.field.add(this.text); - - this.scene.ui.clearText(); - - this.scene.ui.fadeIn(1000).then(() => { - - this.scene.ui.showText("", null, () => { - this.scene.ui.getMessageHandler().bg.setVisible(true); - this.end(); - }, null, true); - }); - } -} -//#endregion - - - - - -//#region 60 UnlockPhase -export class UnlockPhase extends Phase { - private unlockable: Unlockables; - - constructor(scene: BattleScene, unlockable: Unlockables) { - super(scene); - - this.unlockable = unlockable; - } - - start(): void { - this.scene.time.delayedCall(2000, () => { - this.scene.gameData.unlocks[this.unlockable] = true; - this.scene.playSound("level_up_fanfare"); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t("battle:unlockedSomething", { unlockedThing: getUnlockableName(this.unlockable) }), null, () => { - this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true)); - this.end(); - }, null, true, 1500); - }); - } -} -//#endregion - - - - - -//#region 61 PostGameOverPhase -export class PostGameOverPhase extends Phase { - private endCardPhase: EndCardPhase | null; - - constructor(scene: BattleScene, endCardPhase?: EndCardPhase) { - super(scene); - - this.endCardPhase = endCardPhase!; // TODO: is this bang correct? - } - - start() { - super.start(); - - const saveAndReset = () => { - this.scene.gameData.saveAll(this.scene, true, true, true).then(success => { - if (!success) { - return this.scene.reset(true); - } - this.scene.gameData.tryClearSession(this.scene, this.scene.sessionSlotId).then((success: boolean | [boolean, boolean]) => { - if (!success[0]) { - return this.scene.reset(true); - } - this.scene.reset(); - this.scene.unshiftPhase(new TitlePhase(this.scene)); - this.end(); - }); - }); - }; - - if (this.endCardPhase) { - this.scene.ui.fadeOut(500).then(() => { - this.scene.ui.getMessageHandler().bg.setVisible(true); - - this.endCardPhase?.endCard.destroy(); - this.endCardPhase?.text.destroy(); - saveAndReset(); - }); - } else { - saveAndReset(); - } - } -} - -/** - * Opens the party selector UI and transitions into a {@linkcode SwitchSummonPhase} - * for the player (if a switch would be valid for the current battle state). - */ - - - - -//#region 62 SwitchPhase -export class SwitchPhase extends BattlePhase { - protected fieldIndex: integer; - private isModal: boolean; - private doReturn: boolean; - - /** - * Creates a new SwitchPhase - * @param scene {@linkcode BattleScene} Current battle scene - * @param fieldIndex Field index to switch out - * @param isModal Indicates if the switch should be forced (true) or is - * optional (false). - * @param doReturn Indicates if the party member on the field should be - * recalled to ball or has already left the field. Passed to {@linkcode SwitchSummonPhase}. - */ - constructor(scene: BattleScene, fieldIndex: integer, isModal: boolean, doReturn: boolean) { - super(scene); - - this.fieldIndex = fieldIndex; - this.isModal = isModal; - this.doReturn = doReturn; - } - - start() { - super.start(); - - // Skip modal switch if impossible (no remaining party members that aren't in battle) - if (this.isModal && !this.scene.getParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) { - LoggerTools.isPreSwitch.value = false; - LoggerTools.isFaintSwitch.value = false; - return super.end(); - } - - // Skip if the fainted party member has been revived already. doReturn is - // only passed as `false` from FaintPhase (as opposed to other usages such - // as ForceSwitchOutAttr or CheckSwitchPhase), so we only want to check this - // if the mon should have already been returned but is still alive and well - // on the field. see also; battle.test.ts - if (this.isModal && !this.doReturn && !this.scene.getParty()[this.fieldIndex].isFainted()) { - return super.end(); - } - - // Check if there is any space still in field - if (this.isModal && this.scene.getPlayerField().filter(p => p.isAllowedInBattle() && p.isActive(true)).length >= this.scene.currentBattle.getBattlerCount()) { - LoggerTools.isPreSwitch.value = false; - LoggerTools.isFaintSwitch.value = false; - return super.end(); - } - - // Override field index to 0 in case of double battle where 2/3 remaining legal party members fainted at once - const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? this.fieldIndex : 0; - - this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => { - if (this.isModal) {console.error("Forced Switch Detected")} - if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { - if (LoggerTools.isPreSwitch.value) { - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, "Pre-switch" + (option == PartyOption.PASS_BATON ? "+ Baton" : "") + " " + LoggerTools.playerPokeName(this.scene, fieldIndex) + " to " + LoggerTools.playerPokeName(this.scene, slotIndex)) - } else if (LoggerTools.isFaintSwitch.value) { - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, (option == PartyOption.PASS_BATON ? "Baton" : "Send") + " in " + LoggerTools.playerPokeName(this.scene, slotIndex)) - } else { - //LoggerTools.Actions[this.scene.getParty()[fieldIndex].getBattlerIndex()] += " to " + LoggerTools.playerPokeName(this.scene, slotIndex) - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Switch ${LoggerTools.playerPokeName(this.scene, fieldIndex)} to ${LoggerTools.playerPokeName(this.scene, slotIndex)}`) - } - this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, fieldIndex, slotIndex, this.doReturn, option === PartyOption.PASS_BATON)); - } - LoggerTools.isPreSwitch.value = false; - this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); - }, PartyUiHandler.FilterNonFainted); - } -} -//#endregion - - - - - -//#region 63 ExpPhase -export class ExpPhase extends PlayerPartyMemberPokemonPhase { - private expValue: number; - - constructor(scene: BattleScene, partyMemberIndex: integer, expValue: number) { - super(scene, partyMemberIndex); - - this.expValue = expValue; - } - - start() { - super.start(); - - const pokemon = this.getPokemon(); - const exp = new Utils.NumberHolder(this.expValue); - this.scene.applyModifiers(ExpBoosterModifier, true, exp); - exp.value = Math.floor(exp.value); - this.scene.ui.showText(i18next.t("battle:expGain", { pokemonName: getPokemonNameWithAffix(pokemon), exp: exp.value }), null, () => { - const lastLevel = pokemon.level; - pokemon.addExp(exp.value); - const newLevel = pokemon.level; - if (newLevel > lastLevel) { - this.scene.unshiftPhase(new LevelUpPhase(this.scene, this.partyMemberIndex, lastLevel, newLevel)); - } - pokemon.updateInfo().then(() => this.end()); - }, null, true); - } -} -//#endregion - - - - - -//#region 64 ShowPartyExpBarPhase -export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { - private expValue: number; - - constructor(scene: BattleScene, partyMemberIndex: integer, expValue: number) { - super(scene, partyMemberIndex); - - this.expValue = expValue; - } - - start() { - super.start(); - - const pokemon = this.getPokemon(); - const exp = new Utils.NumberHolder(this.expValue); - this.scene.applyModifiers(ExpBoosterModifier, true, exp); - exp.value = Math.floor(exp.value); - - const lastLevel = pokemon.level; - pokemon.addExp(exp.value); - const newLevel = pokemon.level; - if (newLevel > lastLevel) { - this.scene.unshiftPhase(new LevelUpPhase(this.scene, this.partyMemberIndex, lastLevel, newLevel)); - } - this.scene.unshiftPhase(new HidePartyExpBarPhase(this.scene)); - pokemon.updateInfo(); - - if (this.scene.expParty === ExpNotification.SKIP) { - this.end(); - } else if (this.scene.expParty === ExpNotification.ONLY_LEVEL_UP) { - if (newLevel > lastLevel) { // this means if we level up - // instead of displaying the exp gain in the small frame, we display the new level - // we use the same method for mode 0 & 1, by giving a parameter saying to display the exp or the level - this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === ExpNotification.ONLY_LEVEL_UP, newLevel).then(() => { - setTimeout(() => this.end(), 800 / Math.pow(2, this.scene.expGainsSpeed)); - }); - } else { - this.end(); - } - } else if (this.scene.expGainsSpeed < 3) { - this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, false, newLevel).then(() => { - setTimeout(() => this.end(), 500 / Math.pow(2, this.scene.expGainsSpeed)); - }); - } else { - this.end(); - } - - } -} -//#endregion - - - - - -//#region 65 HidePartyExpBarPhase -export class HidePartyExpBarPhase extends BattlePhase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - this.scene.partyExpBar.hide().then(() => this.end()); - } -} -//#endregion - - - - - -//#region 66 LevelUpPhase -export class LevelUpPhase extends PlayerPartyMemberPokemonPhase { - private lastLevel: integer; - private level: integer; - - constructor(scene: BattleScene, partyMemberIndex: integer, lastLevel: integer, level: integer) { - super(scene, partyMemberIndex); - - this.lastLevel = lastLevel; - this.level = level; - this.scene = scene; - } - - start() { - super.start(); - - if (this.level > this.scene.gameData.gameStats.highestLevel) { - this.scene.gameData.gameStats.highestLevel = this.level; - } - - this.scene.validateAchvs(LevelAchv, new Utils.IntegerHolder(this.level)); - - const pokemon = this.getPokemon(); - const prevStats = pokemon.stats.slice(0); - pokemon.calculateStats(); - pokemon.updateInfo(); - if (this.scene.expParty === ExpNotification.DEFAULT) { - this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:levelUp", { pokemonName: getPokemonNameWithAffix(this.getPokemon()), level: this.level }), null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()), null, true); - } else if (this.scene.expParty === ExpNotification.SKIP) { - this.end(); - } else { - // we still want to display the stats if activated - this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()); - } - if (this.lastLevel < 100) { // this feels like an unnecessary optimization - const levelMoves = this.getPokemon().getLevelMoves(this.lastLevel + 1); - for (const lm of levelMoves) { - this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm[1])); - } - } - if (!pokemon.pauseEvolutions) { - const evolution = pokemon.getEvolution(); - if (evolution) { - this.scene.unshiftPhase(new EvolutionPhase(this.scene, pokemon as PlayerPokemon, evolution, this.lastLevel)); - } - } - } -} -//#endregion - - - - - -//#region 67 LearnMovePhase -export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { - private moveId: Moves; - - constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves) { - super(scene, partyMemberIndex); - - this.moveId = moveId; - } - - start() { - super.start(); - - const pokemon = this.getPokemon(); - const move = allMoves[this.moveId]; - - const existingMoveIndex = pokemon.getMoveset().findIndex(m => m?.moveId === move.id); - - if (existingMoveIndex > -1) { - return this.end(); - } - - const emptyMoveIndex = pokemon.getMoveset().length < 4 - ? pokemon.getMoveset().length - : pokemon.getMoveset().findIndex(m => m === null); - - const messageMode = this.scene.ui.getHandler() instanceof EvolutionSceneHandler - ? Mode.EVOLUTION_SCENE - : Mode.MESSAGE; - const noHandler = () => { - this.scene.ui.setMode(messageMode).then(() => { - this.scene.ui.showText(i18next.t("battle:learnMoveStopTeaching", { moveName: move.name }), null, () => { - this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => { - this.scene.ui.setMode(messageMode); - var W = LoggerTools.getWave(LoggerTools.getDRPD(this.scene), this.scene.currentBattle.waveIndex, this.scene) - if (W.shop != "") { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, W.shop + "; skip learning it") - } else { - var actions = LoggerTools.getActionCount(this.scene, this.scene.currentBattle.waveIndex) - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, "Skip " + move.name) - } - this.scene.ui.showText(i18next.t("battle:learnMoveNotLearned", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => this.end(), null, true); - }, (false ? movesFullHandler : () => { - this.scene.ui.setMode(messageMode); - this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId)); - this.end(); - })); - }); - }); - }; - const noHandlerInstant = () => { - this.scene.ui.setMode(messageMode); - var W = LoggerTools.getWave(LoggerTools.getDRPD(this.scene), this.scene.currentBattle.waveIndex, this.scene) - if (W.shop != "") { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, W.shop + "; skip learning it") - } else { - var actions = LoggerTools.getActionCount(this.scene, this.scene.currentBattle.waveIndex) - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, (actions == 0 ? "" : "") + LoggerTools.playerPokeName(this.scene, pokemon) + " | Skip " + move.name) - } - this.scene.ui.showText(i18next.t("battle:learnMoveNotLearned", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => this.end(), null, true); - }; - const movesFullHandler = () => { - this.scene.ui.showText(i18next.t("battle:learnMovePrompt", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => { - this.scene.ui.showText(i18next.t("battle:learnMoveLimitReached", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { - this.scene.ui.showText(i18next.t("battle:learnMoveReplaceQuestion", { moveName: move.name }), null, () => { - this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => { - this.scene.ui.setMode(messageMode); - this.scene.ui.showText(i18next.t("battle:learnMoveForgetQuestion"), null, () => { - this.scene.ui.setModeWithoutClear(Mode.SUMMARY, this.getPokemon(), SummaryUiMode.LEARN_MOVE, move, (moveIndex: integer) => { - if (moveIndex === 4) { - noHandler(); - return; - } - this.scene.ui.setMode(messageMode).then(() => { - this.scene.ui.showText(i18next.t("battle:countdownPoof"), null, () => { - this.scene.ui.showText(i18next.t("battle:learnMoveForgetSuccess", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: pokemon.moveset[moveIndex]!.getName() }), null, () => { // TODO: is the bang correct? - this.scene.ui.showText(i18next.t("battle:learnMoveAnd"), null, () => { - var W = LoggerTools.getWave(LoggerTools.getDRPD(this.scene), this.scene.currentBattle.waveIndex, this.scene) - if (W.shop != "") { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, W.shop + " | " + new PokemonMove(this.moveId).getName() + " → " + pokemon.moveset[moveIndex]!.getName()) - } else { - var actions = LoggerTools.getActionCount(this.scene, this.scene.currentBattle.waveIndex) - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, (actions == 0 ? "" : "") + LoggerTools.playerPokeName(this.scene, pokemon) + " | " + new PokemonMove(this.moveId).getName() + " → " + pokemon.moveset[moveIndex]!.getName()) - } - pokemon.setMove(moveIndex, Moves.NONE); - this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId)); - this.end(); - }, null, true); - }, null, true); - }, null, true); - }); - }); - }, null, true); - }, noHandler); - }); - }, null, true); - }, null, true); - } - if (emptyMoveIndex > -1) { - pokemon.setMove(emptyMoveIndex, this.moveId); - initMoveAnim(this.scene, this.moveId).then(() => { - loadMoveAnimAssets(this.scene, [this.moveId], true) - .then(() => { - this.scene.ui.setMode(messageMode).then(() => { - this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:learnMove", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => { - this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeMoveLearnedTrigger, true); - this.end(); - }, messageMode === Mode.EVOLUTION_SCENE ? 1000 : null, true); - }); - }); - }); - } else if (move.isUnimplemented() && false) { - this.scene.ui.setMode(messageMode).then(() => { - this.scene.ui.showText(`${getPokemonNameWithAffix(pokemon)} wants to learn ${move.name}, but this move does nothing.`, null, () => { - this.scene.ui.showText(`Would you like to teach ${move.name} anyways? (This will be logged as normal)`, null, () => { - this.scene.ui.setModeWithoutClear(Mode.CONFIRM, movesFullHandler, noHandler) - }) - }) - }); - } else { - this.scene.ui.setMode(messageMode).then(movesFullHandler); - } - } -} -//#endregion - - - - - -//#region 68 PokemonHealPhase -export class PokemonHealPhase extends CommonAnimPhase { - private hpHealed: integer; - private message: string | null; - private showFullHpMessage: boolean; - private skipAnim: boolean; - private revive: boolean; - private healStatus: boolean; - private preventFullHeal: boolean; - - constructor(scene: BattleScene, battlerIndex: BattlerIndex, hpHealed: integer, message: string | null, showFullHpMessage: boolean, skipAnim: boolean = false, revive: boolean = false, healStatus: boolean = false, preventFullHeal: boolean = false) { - super(scene, battlerIndex, undefined, CommonAnim.HEALTH_UP); - - this.hpHealed = hpHealed; - this.message = message; - this.showFullHpMessage = showFullHpMessage; - this.skipAnim = skipAnim; - this.revive = revive; - this.healStatus = healStatus; - this.preventFullHeal = preventFullHeal; - } - - start() { - if (!this.skipAnim && (this.revive || this.getPokemon().hp) && !this.getPokemon().isFullHp()) { - super.start(); - } else { - this.end(); - } - } - - end() { - const pokemon = this.getPokemon(); - - if (!pokemon.isOnField() || (!this.revive && !pokemon.isActive())) { - super.end(); - return; - } - - const hasMessage = !!this.message; - const healOrDamage = (!pokemon.isFullHp() || this.hpHealed < 0); - let lastStatusEffect = StatusEffect.NONE; - - if (healOrDamage) { - const hpRestoreMultiplier = new Utils.IntegerHolder(1); - if (!this.revive) { - this.scene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); - } - const healAmount = new Utils.NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value)); - if (healAmount.value < 0) { - pokemon.damageAndUpdate(healAmount.value * -1, HitResult.HEAL as DamageResult); - healAmount.value = 0; - } - // Prevent healing to full if specified (in case of healing tokens so Sturdy doesn't cause a softlock) - if (this.preventFullHeal && pokemon.hp + healAmount.value >= pokemon.getMaxHp()) { - healAmount.value = (pokemon.getMaxHp() - pokemon.hp) - 1; - } - healAmount.value = pokemon.heal(healAmount.value); - if (healAmount.value) { - this.scene.damageNumberHandler.add(pokemon, healAmount.value, HitResult.HEAL); - } - if (pokemon.isPlayer()) { - this.scene.validateAchvs(HealAchv, healAmount); - if (healAmount.value > this.scene.gameData.gameStats.highestHeal) { - this.scene.gameData.gameStats.highestHeal = healAmount.value; - } - } - if (this.healStatus && !this.revive && pokemon.status) { - lastStatusEffect = pokemon.status.effect; - pokemon.resetStatus(); - } - pokemon.updateInfo().then(() => super.end()); - } else if (this.healStatus && !this.revive && pokemon.status) { - lastStatusEffect = pokemon.status.effect; - pokemon.resetStatus(); - pokemon.updateInfo().then(() => super.end()); - } else if (this.showFullHpMessage) { - this.message = i18next.t("battle:hpIsFull", { pokemonName: getPokemonNameWithAffix(pokemon) }); - } - - if (this.message) { - this.scene.queueMessage(this.message); - } - - if (this.healStatus && lastStatusEffect && !hasMessage) { - this.scene.queueMessage(getStatusEffectHealText(lastStatusEffect, getPokemonNameWithAffix(pokemon))); - } - - if (!healOrDamage && !lastStatusEffect) { - super.end(); - } - } -} -//#endregion - - - - - -//#region 69 AttemptCapturePhase -export class AttemptCapturePhase extends PokemonPhase { - /** The Pokeball being used. */ - private pokeballType: PokeballType; - /** The Pokeball sprite. */ - private pokeball: Phaser.GameObjects.Sprite; - /** The sprite's original Y position. */ - private originalY: number; - - constructor(scene: BattleScene, targetIndex: integer, pokeballType: PokeballType) { - super(scene, BattlerIndex.ENEMY + targetIndex); - - this.pokeballType = pokeballType; - } - - roll(y?: integer) { - var roll = (this.getPokemon() as EnemyPokemon).randSeedInt(65536, undefined, "Capture roll") - if (y != undefined) { - console.log(roll, y, roll < y) - } else { - console.log(roll) - } - return roll; - } - - start() { - super.start(); - - const pokemon = this.getPokemon() as EnemyPokemon; - - if (!pokemon?.hp) { - return this.end(); - } - - this.scene.pokeballCounts[this.pokeballType]--; - - this.originalY = pokemon.y; - - const _3m = 3 * pokemon.getMaxHp(); - const _2h = 2 * pokemon.hp; - const catchRate = pokemon.species.catchRate; - const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType); - const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; - const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); - const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); - const fpOffset = pokemon.getFieldPositionOffset(); - - const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType); - this.pokeball = this.scene.addFieldSprite(16, 80, "pb", pokeballAtlasKey); - this.pokeball.setOrigin(0.5, 0.625); - this.scene.field.add(this.pokeball); - - this.scene.playSound("pb_throw"); - this.scene.time.delayedCall(300, () => { - this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon); - }); - - this.scene.tweens.add({ - targets: this.pokeball, - x: { value: 236 + fpOffset[0], ease: "Linear" }, - y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, - duration: 500, - onComplete: () => { - this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); - this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); - this.scene.playSound("pb_rel"); - pokemon.tint(getPokeballTintColor(this.pokeballType)); - - addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType); - - this.scene.tweens.add({ - targets: pokemon, - duration: 500, - ease: "Sine.easeIn", - scale: 0.25, - y: 20, - onComplete: () => { - this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); - pokemon.setVisible(false); - this.scene.playSound("pb_catch"); - this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`)); - - const doShake = () => { - let shakeCount = 0; - const pbX = this.pokeball.x; - const shakeCounter = this.scene.tweens.addCounter({ - from: 0, - to: 1, - repeat: 4, - yoyo: true, - ease: "Cubic.easeOut", - duration: 250, - repeatDelay: 500, - onUpdate: t => { - if (shakeCount && shakeCount < 4) { - const value = t.getValue(); - const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; - this.pokeball.setX(pbX + value * 4 * directionMultiplier); - this.pokeball.setAngle(value * 27.5 * directionMultiplier); - } - }, - onRepeat: () => { - if (!pokemon.species.isObtainable()) { - shakeCounter.stop(); - this.failCatch(shakeCount); - } else if (shakeCount++ < 3) { - if (pokeballMultiplier === -1 || this.roll(y) < y) { - this.scene.playSound("pb_move"); - } else { - shakeCounter.stop(); - this.failCatch(shakeCount); - } - } else { - this.scene.playSound("pb_lock"); - addPokeballCaptureStars(this.scene, this.pokeball); - - const pbTint = this.scene.add.sprite(this.pokeball.x, this.pokeball.y, "pb", "pb"); - pbTint.setOrigin(this.pokeball.originX, this.pokeball.originY); - pbTint.setTintFill(0); - pbTint.setAlpha(0); - this.scene.field.add(pbTint); - this.scene.tweens.add({ - targets: pbTint, - alpha: 0.375, - duration: 200, - easing: "Sine.easeOut", - onComplete: () => { - this.scene.tweens.add({ - targets: pbTint, - alpha: 0, - duration: 200, - easing: "Sine.easeIn", - onComplete: () => pbTint.destroy() - }); - } - }); - } - }, - onComplete: () => { - this.catch(); - } - }); - }; - - this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake)); - } - }); - } - }); - } - - failCatch(shakeCount: integer) { - const pokemon = this.getPokemon(); - - this.scene.playSound("pb_rel"); - pokemon.setY(this.originalY); - if (pokemon.status?.effect !== StatusEffect.SLEEP) { - pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); - } - pokemon.tint(getPokeballTintColor(this.pokeballType)); - pokemon.setVisible(true); - pokemon.untint(250, "Sine.easeOut"); - - const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType); - this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); - this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); - - this.scene.tweens.add({ - targets: pokemon, - duration: 250, - ease: "Sine.easeOut", - scale: 1 - }); - - this.scene.currentBattle.lastUsedPokeball = this.pokeballType; - this.removePb(); - this.end(); - } - - catch() { - /** The Pokemon being caught. */ - const pokemon = this.getPokemon() as EnemyPokemon; - - /** Used for achievements. */ - const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); - - // Achievements - if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) - this.scene.validateAchv(achvs.HIDDEN_ABILITY); - if (pokemon.species.subLegendary) - this.scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); - if (pokemon.species.legendary) - this.scene.validateAchv(achvs.CATCH_LEGENDARY); - if (pokemon.species.mythical) - this.scene.validateAchv(achvs.CATCH_MYTHICAL); - - // Show its info - this.scene.pokemonInfoContainer.show(pokemon, true); - // Update new IVs - this.scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); - - this.scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { - const end = () => { - this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); - this.scene.pokemonInfoContainer.hide(); - this.removePb(); - this.end(); - }; - LoggerTools.logCapture(this.scene, this.scene.currentBattle.waveIndex, pokemon) - const removePokemon = () => { - this.scene.addFaintedEnemyScore(pokemon); - this.scene.getPlayerField().filter(p => p.isActive(true)).forEach(playerPokemon => playerPokemon.removeTagsBySourceId(pokemon.id)); - pokemon.hp = 0; - pokemon.trySetStatus(StatusEffect.FAINT); - this.scene.clearEnemyHeldItemModifiers(); - this.scene.field.remove(pokemon, true); - }; - const addToParty = () => { - const newPokemon = pokemon.addToParty(this.pokeballType); - const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); - if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { - this.scene.validateAchv(achvs.SHINY_PARTY); - } - Promise.all(modifiers.map(m => this.scene.addModifier(m, true))).then(() => { - this.scene.updateModifiers(true); - removePokemon(); - if (newPokemon) { - newPokemon.loadAssets().then(end); - } else { - end(); - } - }); - }; - Promise.all([pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon)]).then(() => { - if (this.scene.getParty().length === 6) { - const promptRelease = () => { - // Say that your party is full - this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { - // Ask if you want to make room - this.scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); - this.scene.ui.setMode(Mode.CONFIRM, () => { - // YES - // Open up the party menu on the RELEASE setting - const newPokemon = this.scene.addPlayerPokemon(pokemon.species, pokemon.level, pokemon.abilityIndex, pokemon.formIndex, pokemon.gender, pokemon.shiny, pokemon.variant, pokemon.ivs, pokemon.nature, pokemon); - this.scene.ui.setMode(Mode.SUMMARY, newPokemon, 0, SummaryUiMode.DEFAULT, () => { - this.scene.ui.setMode(Mode.MESSAGE).then(() => { - promptRelease(); - }); - }, false); - }, () => { - this.scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, this.fieldIndex, (slotIndex: integer, _option: PartyOption) => { - this.scene.ui.setMode(Mode.MESSAGE).then(() => { - if (slotIndex < 6) { - addToParty(); - } else { - promptRelease(); - } - }); - }, undefined, undefined, undefined, undefined, pokemon.name); - }, () => { - // NO - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, "Don't keep " + pokemon.name) - this.scene.ui.setMode(Mode.MESSAGE).then(() => { - removePokemon(); - end(); - }); - }, "fullParty"); - }); - }; - promptRelease(); - } else { - //LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `${pokemon.name} added to party`) - addToParty(); - } - }); - }, 0, true); - } - - /** Remove the Poke Ball from the scene. */ - removePb() { - this.scene.tweens.add({ - targets: this.pokeball, - duration: 250, - delay: 250, - ease: "Sine.easeIn", - alpha: 0, - onComplete: () => this.pokeball.destroy() - }); - } -} -//#endregion - - - - - -//#region 70 AttemptRunPhase` -export class AttemptRunPhase extends PokemonPhase { - constructor(scene: BattleScene, fieldIndex: integer) { - super(scene, fieldIndex); - } - - start() { - super.start(); - - const playerPokemon = this.getPokemon(); - const enemyField = this.scene.getEnemyField(); - - const enemySpeed = enemyField.reduce((total: integer, enemyPokemon: Pokemon) => total + enemyPokemon.getStat(Stat.SPD), 0) / enemyField.length; - - const escapeChance = new Utils.IntegerHolder((((playerPokemon.getStat(Stat.SPD) * 128) / enemySpeed) + (30 * this.scene.currentBattle.escapeAttempts++)) % 256); - applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, true, escapeChance); - - if (playerPokemon.randSeedInt(256, undefined, "Run attempt") < escapeChance.value) { - this.scene.playSound("flee"); - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, "Fled") - this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); - - this.scene.tweens.add({ - targets: [this.scene.arenaEnemy, enemyField].flat(), - alpha: 0, - duration: 250, - ease: "Sine.easeIn", - onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy()) - }); - - this.scene.clearEnemyHeldItemModifiers(); - - enemyField.forEach(enemyPokemon => { - enemyPokemon.hideInfo().then(() => enemyPokemon.destroy()); - enemyPokemon.hp = 0; - enemyPokemon.trySetStatus(StatusEffect.FAINT); - }); - - this.scene.pushPhase(new BattleEndPhase(this.scene)); - this.scene.pushPhase(new NewBattlePhase(this.scene)); - } else { - this.scene.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500); - } - - this.end(); - } -} -//#endregion - - - - - -//#region 71 SelectModifierPhase -const tierNames = [ - "Poké", - "Great", - "Ultra", - "Rogue", - "Master" -] -/** - * This function rolls for modifiers with a certain luck value, checking to see if shiny luck would affect your results. - * @param scene - * @param predictionCost - * @param rerollOverride - * @param modifierOverride - * @returns - */ -export function shinyCheckStep(scene: BattleScene, predictionCost: Utils.IntegerHolder, rerollOverride: integer, modifierOverride?: integer) { - var minLuck = -1 - var modifierPredictions: ModifierTypeOption[][] = [] - const party = scene.getParty(); - regenerateModifierPoolThresholds(party, ModifierPoolType.PLAYER, rerollOverride); - const modifierCount = new Utils.IntegerHolder(3); - scene.applyModifiers(ExtraModifierModifier, true, modifierCount); - if (modifierOverride) { - //modifierCount.value = modifierOverride - } - var isOk = true; - const typeOptions: ModifierTypeOption[] = getPlayerModifierTypeOptions(modifierCount.value, scene.getParty(), undefined, scene, true, true); - typeOptions.forEach((option, idx) => { - let lastTier = option.type!.tier - if (option.alternates && option.alternates.length > 0) { - for (var i = 0; i < option.alternates.length; i++) { - if (option.alternates[i] > lastTier) { - //lastTier = option.alternates[i] - //console.log("Conflict found! (" + i + " luck, " + rerollOverride + " rolls, item " + (idx + 1) + ")") - isOk = false // Shiny Luck affects this wave in some way - if (minLuck == -1 && i != 0) - minLuck = i - } - } - } - }) - modifierPredictions.push(typeOptions) - predictionCost.value += (Math.min(Math.ceil(scene.currentBattle.waveIndex / 10) * 250 * Math.pow(2, rerollOverride), Number.MAX_SAFE_INTEGER)) - return [isOk, minLuck]; -} -/** - * Simulates modifier rolls for as many rerolls as you can afford, checking to see if shiny luck will alter your results. - * @param scene The current `BattleScene`. - * @returns `true` if no changes were detected, `false` otherwise - */ -export function runShinyCheck(scene: BattleScene, mode: integer, wv?: integer) { - var minLuck: integer = -1 - if (mode == 1) { - scene.emulateReset(wv) - } else { - scene.resetSeed(wv); - } - const predictionCost = new Utils.IntegerHolder(0) - var isOk = true; - for (var i = 0; predictionCost.value < scene.money && i < 8; i++) { - var r = shinyCheckStep(scene, predictionCost, i) - isOk = isOk && (r[0] as boolean) - if (isOk || (r[1] as integer) === -1) { - // Do nothing - } else if (minLuck == -1) { - minLuck = (r[1] as integer) - console.log("Luck " + r[1] + " breaks") - } else { - console.log("Updated from " + minLuck + " to " + Math.min(minLuck, (r[1] as integer))) - minLuck = Math.min(minLuck, (r[1] as integer)) - } - } - if (mode == 1) { - scene.restoreSeed(wv) - } else { - scene.resetSeed(wv); - } - if (!isOk) { - console.log("Conflict found!") - } - if (minLuck == 15) { - //minLuck = 0 - } - return [isOk, minLuck] -} -export class SelectModifierPhase extends BattlePhase { - private rerollCount: integer; - private modifierTiers: ModifierTier[] = []; - private modifierPredictions: ModifierTypeOption[][] = [] - private predictionCost: integer = 0; - private costTiers: integer[] = []; - - constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], predictionCost?: integer, modifierPredictions?: ModifierTypeOption[][]) { - super(scene); - - this.rerollCount = rerollCount; - this.modifierTiers = modifierTiers!; // TODO: is this bang correct? - this.modifierPredictions = [] - if (modifierPredictions != undefined) { - this.modifierPredictions = modifierPredictions; - } - this.predictionCost = 0 - this.costTiers = [] - } - - generateSelection(rerollOverride: integer, modifierOverride?: integer, eviolite?: boolean) { - //const STATE = Phaser.Math.RND.state() // Store RNG state - //console.log("====================") - //console.log(" Reroll Prediction: " + rerollOverride) - const party = this.scene.getParty(); - if (eviolite) { - setEvioliteOverride("on") - } else { - setEvioliteOverride("off") - } - regenerateModifierPoolThresholds(party, this.getPoolType(), rerollOverride); - const modifierCount = new Utils.IntegerHolder(3); - if (this.isPlayer()) { - this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); - } - if (modifierOverride) { - //modifierCount.value = modifierOverride - } - const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value, true, true); - setEvioliteOverride("") - typeOptions.forEach((option, idx) => { - option.netprice = this.predictionCost - if (option.type.name == "Nugget") { - option.netprice -= this.scene.getWaveMoneyAmount(1) - } - if (option.type.name == "Big Nugget") { - option.netprice -= this.scene.getWaveMoneyAmount(2.5) - } - if (option.type.name == "Relic Gold") { - option.netprice -= this.scene.getWaveMoneyAmount(10) - } - //console.log(option.type.name) - }) - //console.log("====================") - if (eviolite) { - this.modifierPredictions[rerollOverride].forEach((m, i) => { - if (m.type.name != typeOptions[i].type.name) { - m.eviolite = typeOptions[i].type - } - }) - } else { - this.modifierPredictions[rerollOverride] = typeOptions - } - this.costTiers.push(this.predictionCost) - this.predictionCost += this.getRerollCost(typeOptions, false, rerollOverride) - //Phaser.Math.RND.state(STATE) // Restore RNG state like nothing happened - } - - start() { - super.start(); - - if (!this.rerollCount) { - this.updateSeed(); - console.log(calculateItemConditions(this.scene.getParty(), false, true)) - console.log("\n\nPerforming reroll prediction (Eviolite OFF)\n\n\n") - this.predictionCost = 0 - this.costTiers = [] - for (var idx = 0; idx < 10 && this.predictionCost < this.scene.money; idx++) { - this.generateSelection(idx, undefined, false) - } - this.updateSeed(); - console.log("\n\nPerforming reroll prediction (Eviolite ON)\n\n\n") - this.predictionCost = 0 - this.costTiers = [] - for (var idx = 0; idx < 10 && this.predictionCost < this.scene.money; idx++) { - this.generateSelection(idx, undefined, true) - } - this.updateSeed(); - } else { - this.scene.reroll = false; - } - - const party = this.scene.getParty(); - regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); - const modifierCount = new Utils.IntegerHolder(3); - if (this.isPlayer()) { - this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); - } - const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value); - - const modifierSelectCallback = (rowCursor: integer, cursor: integer) => { - if (rowCursor < 0 || cursor < 0) { - this.scene.ui.showText(i18next.t("battle:skipItemQuestion"), null, () => { - this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, "Skip taking items") - this.scene.ui.revertMode(); - this.scene.ui.setMode(Mode.MESSAGE); - super.end(); - }, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers), this.modifierPredictions)); - }); - return false; - } - let modifierType: ModifierType; - let cost: integer; - switch (rowCursor) { - case 0: - switch (cursor) { - case 0: - const rerollCost1 = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); - if (this.scene.money < rerollCost1) { - this.scene.ui.playError(); - return false; - } else { - this.scene.reroll = true; - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, "Reroll" + (this.scene.lockModifierTiers ? " (Locked)" : "")) - this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as ModifierTier[], this.predictionCost, this.modifierPredictions)); - this.scene.ui.clearText(); - this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); - if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { - this.scene.money -= rerollCost1; - this.scene.updateMoneyText(); - this.scene.animateMoneyChanged(false); - this.scene.playSound("buy"); - } - } - break; - case 0.1: - const rerollCost2 = this.getRerollCost(this.modifierPredictions[this.rerollCount], false); - if (this.scene.money < rerollCost2) { - this.scene.ui.playError(); - return false; - } else { - this.scene.reroll = true; - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, "+1 Reroll") - this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, typeOptions.map(o => o.type!.tier), this.predictionCost, this.modifierPredictions)); - this.scene.ui.clearText(); - this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); - if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { - this.scene.money -= rerollCost2; - this.scene.updateMoneyText(); - this.scene.animateMoneyChanged(false); - this.scene.playSound("buy"); - } - } - break; - case 0.2: - const rerollCost3 = this.getRerollCost(this.modifierPredictions[this.rerollCount + 1], false); - { - this.scene.reroll = true; - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, "-1 Reroll") - this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount - 1, typeOptions.map(o => o.type!.tier), this.predictionCost, this.modifierPredictions)); - this.scene.ui.clearText(); - this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); - if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { - this.scene.money -= rerollCost3; - this.scene.updateMoneyText(); - this.scene.animateMoneyChanged(false); - this.scene.playSound("buy"); - } - } - break; - case 1: - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer, isAll: boolean, isFirst: boolean) => { - if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) { - const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; - const itemModifier = itemModifiers[itemIndex]; - if (isAll) { - if (isFirst) - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Transfer ALL | ${LoggerTools.playerPokeName(this.scene, fromSlotIndex)} → ${LoggerTools.playerPokeName(this.scene, toSlotIndex)}`) - } else { - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Transfer ${itemModifier.type.name + (itemQuantity == itemModifier.getStackCount() ? "" : " x" + itemQuantity)} | ${LoggerTools.playerPokeName(this.scene, fromSlotIndex)} → ${LoggerTools.playerPokeName(this.scene, toSlotIndex)}`) - } - this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); - } else { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - } - }, PartyUiHandler.FilterItemMaxStacks); - break; - case 2: - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - }); - break; - case 3: - this.scene.lockModifierTiers = !this.scene.lockModifierTiers; - const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler; - uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - uiHandler.updateLockRaritiesText(); - uiHandler.updateRerollCostText(); - return false; - } - return true; - case 1: - if (typeOptions[cursor].type) { - modifierType = typeOptions[cursor].type; - } - break; - default: - const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1)); - const shopOption = shopOptions[rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT]; - if (shopOption.type) { - modifierType = shopOption.type; - } - cost = shopOption.cost; - break; - } - - if (cost! && (this.scene.money < cost) && !Overrides.WAIVE_ROLL_FEE_OVERRIDE) { // TODO: is the bang on cost correct? - this.scene.ui.playError(); - return false; - } - - const applyModifier = (modifier: Modifier, playSound: boolean = false) => { - const result = this.scene.addModifier(modifier, false, playSound); - if (cost) { - result.then(success => { - if (success) { - if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { - this.scene.money -= cost; - this.scene.updateMoneyText(); - this.scene.animateMoneyChanged(false); - } - this.scene.playSound("buy"); - (this.scene.ui.getHandler() as ModifierSelectUiHandler).updateCostText(); - } else { - this.scene.ui.playError(); - } - }); - } else { - const doEnd = () => { - this.scene.ui.clearText(); - this.scene.ui.setMode(Mode.MESSAGE); - super.end(); - }; - if (result instanceof Promise) { - result.then(() => doEnd()); - } else { - doEnd(); - } - } - }; - - if (modifierType! instanceof PokemonModifierType) { //TODO: is the bang correct? - if (modifierType instanceof FusePokemonModifierType) { - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.SPLICE, -1, (fromSlotIndex: integer, spliceSlotIndex: integer) => { - if (spliceSlotIndex !== undefined && fromSlotIndex < 6 && spliceSlotIndex < 6 && fromSlotIndex !== spliceSlotIndex) { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, modifierType.name + " → " + this.scene.getParty()[fromSlotIndex].name + " + " + this.scene.getParty()[spliceSlotIndex].name) - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer()).then(() => { - const modifier = modifierType.newModifier(party[fromSlotIndex], party[spliceSlotIndex])!; //TODO: is the bang correct? - applyModifier(modifier, true); - }); - } else { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - } - }, modifierType.selectFilter); - } else { - const pokemonModifierType = modifierType as PokemonModifierType; - const isMoveModifier = modifierType instanceof PokemonMoveModifierType; - const isTmModifier = modifierType instanceof TmModifierType; - const isRememberMoveModifier = modifierType instanceof RememberMoveModifierType; - const isPpRestoreModifier = (modifierType instanceof PokemonPpRestoreModifierType || modifierType instanceof PokemonPpUpModifierType); - const partyUiMode = isMoveModifier ? PartyUiMode.MOVE_MODIFIER - : isTmModifier ? PartyUiMode.TM_MODIFIER - : isRememberMoveModifier ? PartyUiMode.REMEMBER_MOVE_MODIFIER - : PartyUiMode.MODIFIER; - const tmMoveId = isTmModifier - ? (modifierType as TmModifierType).moveId - : undefined; - this.scene.ui.setModeWithoutClear(Mode.PARTY, partyUiMode, -1, (slotIndex: integer, option: PartyOption) => { - if (slotIndex < 6) { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer()).then(() => { - const modifier = !isMoveModifier - ? !isRememberMoveModifier - ? modifierType.newModifier(party[slotIndex]) - : modifierType.newModifier(party[slotIndex], option as integer) - : modifierType.newModifier(party[slotIndex], option - PartyOption.MOVE_1); - if (isPpRestoreModifier) { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, modifierType.name + " → " + this.scene.getParty()[slotIndex].name + " → " + this.scene.getParty()[slotIndex].moveset[option - PartyOption.MOVE_1]!.getName()) - } else if (isRememberMoveModifier) { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, modifierType.name + " → " + this.scene.getParty()[slotIndex].name) - } else if (isTmModifier) { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, modifierType.name + " → " + this.scene.getParty()[slotIndex].name) - } else { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, modifierType.name + " → " + this.scene.getParty()[slotIndex].name) - } - applyModifier(modifier!, true); // TODO: is the bang correct? - }); - } else { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - } - }, pokemonModifierType.selectFilter, modifierType instanceof PokemonMoveModifierType ? (modifierType as PokemonMoveModifierType).moveSelectFilter : undefined, tmMoveId, isPpRestoreModifier); - } - } else { - LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, modifierType!.name) - applyModifier(modifierType!.newModifier()!); // TODO: is the bang correct? - } - - return !cost!;// TODO: is the bang correct? - }; - if (this.rerollCount == 0) { - if (true) { - this.modifierPredictions.forEach((mp, r) => { - // costTiers - console.log("Rerolls: " + r + (this.costTiers[r] != 0 ? " - ₽" + this.costTiers[r] : "")) - mp.forEach((m, i) => { - console.log(" " + m.type!.name + (m.netprice != this.costTiers[r] ? " - ₽" + m.netprice : "")) - if (m.eviolite) { - console.log(" With Eviolite unlocked: " + m.eviolite.name) - } - if (m.alternates) { - //console.log(m.alternates) - let showedLuckFlag = false - for (var j = 0, currentTier = m.type!.tier; j < m.alternates.length; j++) { - if (m.alternates[j] > currentTier) { - currentTier = m.alternates[j] - if (m.advancedAlternates) { - if (!showedLuckFlag) { - showedLuckFlag = true - console.log(" Your luck: " + getPartyLuckValue(party) + " (" + getLuckString(getPartyLuckValue(party)) + ")") - } - console.log(" At " + j + " luck (" + getLuckString(j) + "): " + m.advancedAlternates[j]) - } else { - if (!showedLuckFlag) { - showedLuckFlag = true - console.log(" Your luck: " + getPartyLuckValue(party) + " (" + getLuckString(getPartyLuckValue(party)) + ")") - } - console.log(" At " + j + " luck (" + getLuckString(j) + "): " + tierNames[currentTier] + "-tier item (failed to generate item)") - } - } - } - } else { - //console.log(" No alt-luck data") - } - }) - }) - } else { - let modifierList: string[] = [] - this.modifierPredictions.forEach((mp, r) => { - //console.log("Rerolls: " + r) - mp.forEach((m, i) => { - modifierList.push(m.type!.name + (r > 0 ? " (x" + r + ")" : "")) - //console.log(" " + m.type!.name) - if (m.eviolite) { - modifierList.push(m.type!.name + (r > 0 ? " (x" + r + " with eviolite unlocked)" : " (With eviolite unlocked)")) - //console.log(" With Eviolite unlocked: " + m.eviolite.name) - } - if (m.alternates) { - //console.log(m.alternates) - let showedLuckFlag = false - for (var j = 0, currentTier = m.type!.tier; j < m.alternates.length; j++) { - if (m.alternates[j] > currentTier) { - currentTier = m.alternates[j] - if (m.advancedAlternates) { - if (!showedLuckFlag) { - showedLuckFlag = true - console.log(" Your luck: " + getPartyLuckValue(party) + " (" + getLuckString(getPartyLuckValue(party)) + ")") - } - console.log(" At " + j + " luck (" + getLuckString(j) + "): " + m.advancedAlternates[j]) - } else { - if (!showedLuckFlag) { - showedLuckFlag = true - console.log(" Your luck: " + getPartyLuckValue(party) + " (" + getLuckString(getPartyLuckValue(party)) + ")") - } - console.log(" At " + j + " luck (" + getLuckString(j) + "): " + tierNames[currentTier] + "-tier item (failed to generate item)") - } - } - } - } else { - //console.log(" No alt-luck data") - } - }) - }) - modifierList.sort() - modifierList.forEach(v => { - console.log(v) - }) - } - } - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - } - - updateSeed(): void { - this.scene.resetSeed(); - } - - isPlayer(): boolean { - return true; - } - - getRerollCost(typeOptions: ModifierTypeOption[], lockRarities: boolean, rerollOverride?: integer): integer { - let baseValue = 0; - if (Overrides.WAIVE_ROLL_FEE_OVERRIDE) { - return baseValue; - } else if (lockRarities) { - const tierValues = [50, 125, 300, 750, 2000]; - for (const opt of typeOptions) { - baseValue += tierValues[opt.type.tier ?? 0]; - } - } else { - baseValue = 250; - } - return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, (rerollOverride != undefined ? rerollOverride : this.rerollCount)), Number.MAX_SAFE_INTEGER); - } - - getPoolType(): ModifierPoolType { - return ModifierPoolType.PLAYER; - } - - getModifierTypeOptions(modifierCount: integer, shutUpBro?: boolean, calcAllLuck?: boolean, advanced?: boolean): ModifierTypeOption[] { - return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.scene, shutUpBro, calcAllLuck, advanced); - } - - addModifier(modifier: Modifier): Promise { - return this.scene.addModifier(modifier, false, true); - } -} -//#endregion - - - - - -//#region 72 EggLapsePhase -export class EggLapsePhase extends Phase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - const eggsToHatch: Egg[] = this.scene.gameData.eggs.filter((egg: Egg) => { - return Overrides.EGG_IMMEDIATE_HATCH_OVERRIDE ? true : --egg.hatchWaves < 1; - }); - - let eggCount: integer = eggsToHatch.length; - - if (eggCount) { - this.scene.queueMessage(i18next.t("battle:eggHatching")); - - for (const egg of eggsToHatch) { - this.scene.unshiftPhase(new EggHatchPhase(this.scene, egg, eggCount)); - if (eggCount > 0) { - eggCount--; - } - } - - } - this.end(); - } -} -//#endregion - - - - - -//#region 73 AddEnemyBuffModifierPhase -export class AddEnemyBuffModifierPhase extends Phase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - const waveIndex = this.scene.currentBattle.waveIndex; - const tier = !(waveIndex % 1000) ? ModifierTier.ULTRA : !(waveIndex % 250) ? ModifierTier.GREAT : ModifierTier.COMMON; - - regenerateModifierPoolThresholds(this.scene.getEnemyParty(), ModifierPoolType.ENEMY_BUFF); - - const count = Math.ceil(waveIndex / 250); - for (let i = 0; i < count; i++) { - this.scene.addEnemyModifier(getEnemyBuffModifierForWave(tier, this.scene.findModifiers(m => m instanceof EnemyPersistentModifier, false), this.scene), true, true); - } - this.scene.updateModifiers(false, true).then(() => this.end()); - } -} -//#endregion - - - - - -//#region 74 PartyStatusCurePhase -/** - * Cures the party of all non-volatile status conditions, shows a message - * @param {BattleScene} scene The current scene - * @param {Pokemon} user The user of the move that cures the party - * @param {string} message The message that should be displayed - * @param {Abilities} abilityCondition Pokemon with this ability will not be affected ie. Soundproof - */ -export class PartyStatusCurePhase extends BattlePhase { - private user: Pokemon; - private message: string; - private abilityCondition: Abilities; - - constructor(scene: BattleScene, user: Pokemon, message: string, abilityCondition: Abilities) { - super(scene); - - this.user = user; - this.message = message; - this.abilityCondition = abilityCondition; - } - - start() { - super.start(); - for (const pokemon of this.scene.getParty()) { - if (!pokemon.isOnField() || pokemon === this.user) { - pokemon.resetStatus(false); - pokemon.updateInfo(true); - } else { - if (!pokemon.hasAbility(this.abilityCondition)) { - pokemon.resetStatus(); - pokemon.updateInfo(true); - } else { - // Manually show ability bar, since we're not hooked into the targeting system - pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, pokemon.getPassiveAbility()?.id === this.abilityCondition)); - } - } - } - if (this.message) { - this.scene.queueMessage(this.message); - } - this.end(); - } -} -//#endregion - - - - - -//#region 75 PartyHealPhase -export class PartyHealPhase extends BattlePhase { - private resumeBgm: boolean; - - constructor(scene: BattleScene, resumeBgm: boolean) { - super(scene); - - this.resumeBgm = resumeBgm; - } - - start() { - super.start(); - - const bgmPlaying = this.scene.isBgmPlaying(); - if (bgmPlaying) { - this.scene.fadeOutBgm(1000, false); - } - this.scene.ui.fadeOut(1000).then(() => { - for (const pokemon of this.scene.getParty()) { - pokemon.hp = pokemon.getMaxHp(); - pokemon.resetStatus(); - for (const move of pokemon.moveset) { - move!.ppUsed = 0; // TODO: is this bang correct? - } - pokemon.updateInfo(true); - } - const healSong = this.scene.playSoundWithoutBgm("heal"); - this.scene.time.delayedCall(Utils.fixedInt(healSong.totalDuration * 1000), () => { - healSong.destroy(); - if (this.resumeBgm && bgmPlaying) { - this.scene.playBgm(); - } - this.scene.ui.fadeIn(500).then(() => this.end()); - }); - }); - } -} -//#endregion - - - - - -//#region 76 ShinySparklePhase -export class ShinySparklePhase extends PokemonPhase { - constructor(scene: BattleScene, battlerIndex: BattlerIndex) { - super(scene, battlerIndex); - } - - start() { - super.start(); - - this.getPokemon().sparkle(); - this.scene.time.delayedCall(1000, () => this.end()); - } -} -//#endregion - - - - - -//#region 77 ScanIvsPhase -export class ScanIvsPhase extends PokemonPhase { - private shownIvs: integer; - - constructor(scene: BattleScene, battlerIndex: BattlerIndex, shownIvs: integer) { - super(scene, battlerIndex); - - this.shownIvs = shownIvs; - } - - start() { - super.start(); - - if (!this.shownIvs) { - return this.end(); - } - - const pokemon = this.getPokemon(); - - let enemyIvs: number[] = []; - let statsContainer: Phaser.GameObjects.Sprite[] = []; - let statsContainerLabels: Phaser.GameObjects.Sprite[] = []; - const enemyField = this.scene.getEnemyField(); - const uiTheme = (this.scene as BattleScene).uiTheme; // Assuming uiTheme is accessible - for (let e = 0; e < enemyField.length; e++) { - enemyIvs = enemyField[e].ivs; - const currentIvs = this.scene.gameData.dexData[enemyField[e].species.getRootSpeciesId()].ivs; // we are using getRootSpeciesId() here because we want to check against the baby form, not the mid form if it exists - const ivsToShow = this.scene.ui.getMessageHandler().getTopIvs(enemyIvs, this.shownIvs); - statsContainer = enemyField[e].getBattleInfo().getStatsValueContainer().list as Phaser.GameObjects.Sprite[]; - statsContainerLabels = statsContainer.filter(m => m.name.indexOf("icon_stat_label") >= 0); - for (let s = 0; s < statsContainerLabels.length; s++) { - const ivStat = Stat[statsContainerLabels[s].frame.name]; - if (enemyIvs[ivStat] > currentIvs[ivStat] && ivsToShow.indexOf(Number(ivStat)) >= 0) { - const hexColour = enemyIvs[ivStat] === 31 ? getTextColor(TextStyle.PERFECT_IV, false, uiTheme) : getTextColor(TextStyle.SUMMARY_GREEN, false, uiTheme); - const hexTextColour = Phaser.Display.Color.HexStringToColor(hexColour).color; - statsContainerLabels[s].setTint(hexTextColour); - } - statsContainerLabels[s].setVisible(true); - } - } - - if (!this.scene.hideIvs) { - this.scene.ui.showText(i18next.t("battle:ivScannerUseQuestion", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { - this.scene.ui.setMode(Mode.CONFIRM, () => { - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, "IV Scanner → " + LoggerTools.enemyPokeName(this.scene, pokemon)) - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.clearText(); - new CommonBattleAnim(CommonAnim.LOCK_ON, pokemon, pokemon).play(this.scene, () => { - this.scene.ui.getMessageHandler().promptIvs(pokemon.id, pokemon.ivs, this.shownIvs).then(() => this.end()); - }); - }, () => { - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.clearText(); - this.end(); - }); - }); - } else { - this.end(); - } - } -} -//#endregion - - - - - -//#region 78 TrainerMessageTestPhase -export class TrainerMessageTestPhase extends BattlePhase { - private trainerTypes: TrainerType[]; - - constructor(scene: BattleScene, ...trainerTypes: TrainerType[]) { - super(scene); - - this.trainerTypes = trainerTypes; - } - - start() { - super.start(); - - const testMessages: string[] = []; - - for (const t of Object.keys(trainerConfigs)) { - const type = parseInt(t); - if (this.trainerTypes.length && !this.trainerTypes.find(tt => tt === type as TrainerType)) { - continue; - } - const config = trainerConfigs[type]; - [config.encounterMessages, config.femaleEncounterMessages, config.victoryMessages, config.femaleVictoryMessages, config.defeatMessages, config.femaleDefeatMessages] - .map(messages => { - if (messages?.length) { - testMessages.push(...messages); - } - }); - } - - for (const message of testMessages) { - this.scene.pushPhase(new TestMessagePhase(this.scene, message)); - } - - this.end(); - } -} -//#endregion - - - - - -//#region 79 TestMessagePhase -export class TestMessagePhase extends MessagePhase { - constructor(scene: BattleScene, message: string) { - super(scene, message, null, true); - } -} -//#endregion \ No newline at end of file diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index a3ecf305254..fa9f41fe3e6 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -73,6 +73,7 @@ export class CheckSwitchPhase extends BattlePhase { this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { + // Yes, I want to Pre-Switch this.scene.ui.setMode(Mode.MESSAGE); LoggerTools.isPreSwitch.value = true this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); @@ -89,6 +90,7 @@ export class CheckSwitchPhase extends BattlePhase { //this.scene.pokemonInfoContainer.hide() this.end(); }, () => { + // No, I want to leave my Pokémon as is this.scene.ui.setMode(Mode.MESSAGE); for (var i = 0; i < this.scene.getEnemyField().length; i++) { this.scene.getEnemyField()[i].getBattleInfo().flyoutMenu.toggleFlyout(false) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 8c12b7d43ad..edc6a70ad92 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -1,5 +1,5 @@ import BattleScene from "#app/battle-scene"; -import { TurnCommand, BattleType } from "#app/battle"; +import { TurnCommand, BattleType, BattlerIndex } from "#app/battle"; import { TrappedTag, EncoreTag } from "#app/data/battler-tags"; import { MoveTargetSet, getMoveTargets } from "#app/data/move"; import { speciesStarters } from "#app/data/pokemon-species"; @@ -8,7 +8,7 @@ import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Biome } from "#app/enums/biome"; import { Moves } from "#app/enums/moves"; import { PokeballType } from "#app/enums/pokeball"; -import { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; +import { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { Command } from "#app/ui/command-ui-handler"; import { Mode } from "#app/ui/ui"; @@ -17,7 +17,9 @@ import { FieldPhase } from "./field-phase"; import { SelectTargetPhase } from "./select-target-phase"; import * as LoggerTools from "../logger"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import { isNullOrUndefined } from "#app/utils"; +import { getEnumKeys, isNullOrUndefined } from "#app/utils"; + +export const targIDs: string[] = ["Self", "Self", "Ally", "L", "R", "Self", "Ally"] export class CommandPhase extends FieldPhase { protected fieldIndex: integer; @@ -39,6 +41,11 @@ export class CommandPhase extends FieldPhase { } else { const allyCommand = this.scene.currentBattle.turnCommands[this.fieldIndex - 1]; if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) { + if (this.fieldIndex == 0) { + LoggerTools.Actions[1] = ""; // Remove the second Pokémon's action, as we will not be attacking this turn + } else { + LoggerTools.Actions[0] = "" // Remove the first Pokémon's action, as their turn is now being skipped] + } this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: allyCommand?.command, skip: true }; } } @@ -52,6 +59,7 @@ export class CommandPhase extends FieldPhase { const moveQueue = playerPokemon.getMoveQueue(); + // Remove queued moves the Pokemon no longer has access to or can't use this turn while (moveQueue.length && moveQueue[0] && moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move) || !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move)]!.isUsable(playerPokemon, moveQueue[0].ignorePP))) { // TODO: is the bang correct? @@ -61,12 +69,16 @@ export class CommandPhase extends FieldPhase { if (moveQueue.length) { const queuedMove = moveQueue[0]; if (!queuedMove.move) { - this.handleCommand(Command.FIGHT, -1, false); + // Struggle + this.handleCommand(Command.FIGHT, false, -1, false); } else { + // Locate the queued move in our moveset const moveIndex = playerPokemon.getMoveset().findIndex(m => m?.moveId === queuedMove.move); if (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex]!.isUsable(playerPokemon, queuedMove.ignorePP)) { // TODO: is the bang correct? - this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, { targets: queuedMove.targets, multiple: queuedMove.targets.length > 1 }); + // Use the queued move + this.handleCommand(Command.FIGHT, false, moveIndex, queuedMove.ignorePP, { targets: queuedMove.targets, multiple: queuedMove.targets.length > 1 }); } else { + // The move is no longer in our moveset or is unuseable; allow the player to choose an action this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); } } @@ -80,10 +92,14 @@ export class CommandPhase extends FieldPhase { } } - handleCommand(command: Command, cursor: integer, ...args: any[]): boolean { + handleCommand(command: Command, logCommand: boolean = true, cursor: integer, ...args: any[]): boolean { const playerPokemon = this.scene.getPlayerField()[this.fieldIndex]; let success: boolean; + if (!logCommand) { + LoggerTools.Actions[this.fieldIndex] = "%SKIP" + } + switch (command) { case Command.FIGHT: let useStruggle = false; @@ -93,19 +109,26 @@ export class CommandPhase extends FieldPhase { const moveId = !useStruggle ? cursor > -1 ? playerPokemon.getMoveset()[cursor]!.moveId : Moves.NONE : Moves.STRUGGLE; // TODO: is the bang correct? const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args }; const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, moveId) : args[2]; + let moveData: PokemonMove | undefined; if (!moveId) { turnCommand.targets = [this.fieldIndex]; + } else { + moveData = new PokemonMove(moveId, 0, 0, true); } console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); if (moveTargets.targets.length > 1 && moveTargets.multiple) { this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)); + // No need to log the move, as SelectTargetPhase will call another CommandPhase with the correct data } if (moveTargets.targets.length <= 1 || moveTargets.multiple) { turnCommand.move!.targets = moveTargets.targets; //TODO: is the bang correct here? + LoggerTools.Actions[this.fieldIndex] = (moveData ? moveData!.getName() : "[???]") + " " + this.formatTargets([], this.fieldIndex) } else if (playerPokemon.getTag(BattlerTagType.CHARGING) && playerPokemon.getMoveQueue().length >= 1) { + // A charging move will be executed this turn, so we do not need to log ourselves using it (we already selected the move last turn) turnCommand.move!.targets = playerPokemon.getMoveQueue()[0].targets; //TODO: is the bang correct here? } else { this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)); + // No need to log the move, as SelectTargetPhase will call another CommandPhase with the correct data } this.scene.currentBattle.turnCommands[this.fieldIndex] = turnCommand; success = true; @@ -170,7 +193,9 @@ export class CommandPhase extends FieldPhase { } else { this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.BALL, cursor: cursor }; this.scene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets; + LoggerTools.Actions[this.fieldIndex] = "Ball" if (this.fieldIndex) { + LoggerTools.Actions.shift() this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; } success = true; @@ -180,10 +205,15 @@ export class CommandPhase extends FieldPhase { break; case Command.POKEMON: case Command.RUN: + // We are attempting to switch Pokémon or run from battle + /** Are we attempting to switch (`true`) or run away (`false`)? */ const isSwitch = command === Command.POKEMON; + /** Pulls data from the `BattleScene` */ const { currentBattle, arena } = this.scene; + /** Whether the player can flee from this Mystery Encounter */ const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed; if (!isSwitch && (arena.biomeType === Biome.END || (!isNullOrUndefined(mysteryEncounterFleeAllowed) && !mysteryEncounterFleeAllowed))) { + // We are attempting to run away, and we are either in ???, or in a Mystery Encounter we're not allowed to flee from this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.showText(i18next.t("battle:noEscapeForce"), null, () => { @@ -191,6 +221,7 @@ export class CommandPhase extends FieldPhase { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); } else if (!isSwitch && (currentBattle.battleType === BattleType.TRAINER || currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE)) { + // We are attempting to run away from a Trainer Battle this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.showText(i18next.t("battle:noEscapeTrainer"), null, () => { @@ -198,17 +229,29 @@ export class CommandPhase extends FieldPhase { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); } else { + // We are attempting to switch, or are attempting to run away and the game isn't preventing it + /** + * Whether we are attempting to Baton Pass. + * + * If we are running away, not switching, this value is `false`. */ const batonPass = isSwitch && args[0] as boolean; + /** Stores reasons why you cannot run away or switch your Trapped Pokémon. */ const trappedAbMessages: string[] = []; - if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { + if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { // We are attempting to Baton Pass currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; success = true; + LoggerTools.Actions[this.fieldIndex] = isSwitch ? `Switch to ${this.scene.getParty()[cursor].name}` : "Run from battle" if (!isSwitch && this.fieldIndex) { + // If we're trying to run away, and we are in the second field slot, skip the first Pokémon's turn currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; } - } else if (trappedAbMessages.length > 0) { + } else if (trappedAbMessages.length > 0) { // We are attempting to Baton Pass, but we were trapped during the turn and can't do so anymore + // This is actually genius wow + // If batonPass was true, playerPokemon.isTrapped() will run + // If this function also returns true, the top block is skipped, and this block runs + // If batonPass is false, playerPokemon.isTrapped() will not run, which means trappedAbMessages.length is 0, and this block is skipped, too if (!isSwitch) { this.scene.ui.setMode(Mode.MESSAGE); } @@ -218,14 +261,16 @@ export class CommandPhase extends FieldPhase { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); } }, null, true); - } else { + } else { // We are not attempting to Baton Pass const trapTag = playerPokemon.getTag(TrappedTag); // trapTag should be defined at this point, but just in case... if (!trapTag) { + // If the Pokémon is not trapped, break from this block and set the action currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; + LoggerTools.Actions[this.fieldIndex] = isSwitch ? `Switch to ${this.scene.getParty()[cursor].name}` : "Run from battle" break; } @@ -260,7 +305,9 @@ export class CommandPhase extends FieldPhase { cancel() { if (this.fieldIndex) { + LoggerTools.Actions[0] = "" this.scene.unshiftPhase(new CommandPhase(this.scene, 0)); + LoggerTools.Actions[1] = "" this.scene.unshiftPhase(new CommandPhase(this.scene, 1)); this.end(); } @@ -281,11 +328,39 @@ export class CommandPhase extends FieldPhase { return false; } - this.handleCommand(Command.FIGHT, moveIndex, false); + // Select the forced move (it is not logged since the player cannot make a choice here) + this.handleCommand(Command.FIGHT, false, moveIndex, false); return true; } + /** + * Formats an attack's targets. + * @param indices The `BattlerIndex` of all targets + * @param fieldIndex The `BattlerIndex` of the user + */ + formatTargets(indices: BattlerIndex[], fieldIndex: BattlerIndex) { + var output: string[] = []; + for (var i = 0; i < indices.length; i++) { + var selection = ""; + if (fieldIndex < 2) { + // Player + selection = targIDs[indices[i] + 1]; + } else { + // Enemy + selection = targIDs[indices[i] + 3]; + } + // If this Pokémon is on the right side of the field, flip the terms 'self' and 'ally' + if (selection == "Self" && fieldIndex % 2 == 1) { + selection = "Ally" + } + if (selection == "Ally" && fieldIndex % 2 == 1) { + selection = "Self" + } + } + return "→ " + output.join(", "); + } + getFieldIndex(): integer { return this.fieldIndex; } diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 8dc4d2e5d3c..5b30caab55a 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -109,14 +109,14 @@ export class FaintPhase extends PokemonPhase { * push a phase that prompts the player to summon a Pokemon from their party. */ LoggerTools.isFaintSwitch.value = true; - this.scene.pushPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, true, false)); + this.scene.pushPhase(new SwitchPhase(this.scene, SwitchType.MID_TURN_SWITCH, this.fieldIndex, true, false)); } } else { this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) { const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length; if (hasReservePartyMember) { - this.scene.pushPhase(new SwitchSummonPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, -1, false, false)); + this.scene.pushPhase(new SwitchSummonPhase(this.scene, SwitchType.MID_TURN_SWITCH, this.fieldIndex, -1, false, false)); } } } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 60755095cca..a3e241d1b89 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -242,7 +242,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { const playerField = this.scene.getPlayerField(); playerField.forEach((pokemon, i) => { if (!pokemon.isAllowedInBattle() && legalPlayerPartyPokemon.length > i) { - this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, i, true, false)); + this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.MID_TURN_SWITCH, i, true, false)); } }); diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 1ad27eb12d9..930bccb7585 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -17,21 +17,18 @@ export class SelectModifierPhase extends BattlePhase { private rerollCount: integer; private modifierTiers?: ModifierTier[]; private customModifierSettings?: CustomModifierSettings; - private modifierPredictions: ModifierTypeOption[][] = []; + private modifierPredictions?: ModifierTypeOption[][] = []; private predictionCost: integer = 0; private costTiers: integer[] = []; - constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings) { + constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings, predictionCost: integer = 0, modifierPredictions?: ModifierTypeOption[][]) { super(scene); this.rerollCount = rerollCount; this.modifierTiers = modifierTiers; this.customModifierSettings = customModifierSettings; - this.modifierPredictions = [] - if (modifierPredictions != undefined) { - this.modifierPredictions = modifierPredictions; - } - this.predictionCost = 0 + this.modifierPredictions = modifierPredictions; + this.predictionCost = predictionCost this.costTiers = [] } @@ -46,7 +43,7 @@ export class SelectModifierPhase extends BattlePhase { this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); } if (modifierOverride) { - //modifierCount.value = modifierOverride + modifierCount.value = modifierOverride } const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value); typeOptions.forEach((option, idx) => { @@ -63,9 +60,11 @@ export class SelectModifierPhase extends BattlePhase { //console.log(option.type.name) }) //console.log("====================") - this.modifierPredictions[rerollOverride] = typeOptions - this.costTiers.push(this.predictionCost) - this.predictionCost += this.getRerollCost(typeOptions, false, rerollOverride) + if (this.modifierPredictions != undefined) { + this.modifierPredictions[rerollOverride] = typeOptions + this.costTiers.push(this.predictionCost) + this.predictionCost += this.getRerollCost(typeOptions, false, rerollOverride) + } //Phaser.Math.RND.state(STATE) // Restore RNG state like nothing happened } @@ -296,7 +295,9 @@ export class SelectModifierPhase extends BattlePhase { return !cost!;// TODO: is the bang correct? }; if (this.rerollCount == 0) { - if (true) { + if (this.modifierPredictions == undefined) { + // Do nothing + } else if (true) { this.modifierPredictions.forEach((mp, r) => { // costTiers console.log("Rerolls: " + r + (this.costTiers[r] != 0 ? " - ₽" + this.costTiers[r] : "")) @@ -305,73 +306,8 @@ export class SelectModifierPhase extends BattlePhase { if (m.eviolite) { console.log(" With Eviolite unlocked: " + m.eviolite.name) } - if (m.alternates) { - //console.log(m.alternates) - let showedLuckFlag = false - for (var j = 0, currentTier = m.type!.tier; j < m.alternates.length; j++) { - if (m.alternates[j] > currentTier) { - currentTier = m.alternates[j] - if (m.advancedAlternates) { - if (!showedLuckFlag) { - showedLuckFlag = true - console.log(" Your luck: " + getPartyLuckValue(party) + " (" + getLuckString(getPartyLuckValue(party)) + ")") - } - console.log(" At " + j + " luck (" + getLuckString(j) + "): " + m.advancedAlternates[j]) - } else { - if (!showedLuckFlag) { - showedLuckFlag = true - console.log(" Your luck: " + getPartyLuckValue(party) + " (" + getLuckString(getPartyLuckValue(party)) + ")") - } - console.log(" At " + j + " luck (" + getLuckString(j) + "): " + LoggerTools.tierNames[currentTier] + "-tier item")// (failed to generate item) - } - } - } - } else { - //console.log(" No alt-luck data") - } }) }) - } else { - let modifierList: string[] = [] - this.modifierPredictions.forEach((mp, r) => { - //console.log("Rerolls: " + r) - mp.forEach((m, i) => { - modifierList.push(m.type!.name + (r > 0 ? " (x" + r + ")" : "")) - //console.log(" " + m.type!.name) - if (m.eviolite) { - modifierList.push(m.type!.name + (r > 0 ? " (x" + r + " with eviolite unlocked)" : " (With eviolite unlocked)")) - //console.log(" With Eviolite unlocked: " + m.eviolite.name) - } - if (m.alternates) { - //console.log(m.alternates) - let showedLuckFlag = false - for (var j = 0, currentTier = m.type!.tier; j < m.alternates.length; j++) { - if (m.alternates[j] > currentTier) { - currentTier = m.alternates[j] - if (m.advancedAlternates) { - if (!showedLuckFlag) { - showedLuckFlag = true - console.log(" Your luck: " + getPartyLuckValue(party) + " (" + getLuckString(getPartyLuckValue(party)) + ")") - } - console.log(" At " + j + " luck (" + getLuckString(j) + "): " + m.advancedAlternates[j]) - } else { - if (!showedLuckFlag) { - showedLuckFlag = true - console.log(" Your luck: " + getPartyLuckValue(party) + " (" + getLuckString(getPartyLuckValue(party)) + ")") - } - console.log(" At " + j + " luck (" + getLuckString(j) + "): " + LoggerTools.tierNames[currentTier] + "-tier item (failed to generate item)") - } - } - } - } else { - //console.log(" No alt-luck data") - } - }) - }) - modifierList.sort() - modifierList.forEach(v => { - console.log(v) - }) } } this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); diff --git a/src/phases/select-target-phase.ts b/src/phases/select-target-phase.ts index b3eeb22eb7b..8114e296c1b 100644 --- a/src/phases/select-target-phase.ts +++ b/src/phases/select-target-phase.ts @@ -2,7 +2,7 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; import { Command } from "#app/ui/command-ui-handler"; import { Mode } from "#app/ui/ui"; -import { CommandPhase } from "./command-phase"; +import { CommandPhase, targIDs } from "./command-phase"; import { PokemonPhase } from "./pokemon-phase"; import * as LoggerTools from "../logger"; import i18next from "#app/plugins/i18n"; @@ -32,9 +32,11 @@ export class SelectTargetPhase extends PokemonPhase { // Cancel this action if (targets.length < 1) { this.scene.currentBattle.turnCommands[this.fieldIndex] = null; + LoggerTools.Actions[this.fieldIndex] = ""; this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex)); } else { turnCommand!.targets = targets; //TODO: is the bang correct here? + LoggerTools.Actions[this.fieldIndex] += " " + this.formatTargets(targets, this.fieldIndex) } if (turnCommand?.command === Command.BALL && this.fieldIndex) { this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; //TODO: is the bang correct here? @@ -42,4 +44,31 @@ export class SelectTargetPhase extends PokemonPhase { this.end(); }); } + + /** + * Formats an attack's targets. + * @param indices The `BattlerIndex` of all targets + * @param fieldIndex The `BattlerIndex` of the user + */ + formatTargets(indices: BattlerIndex[], fieldIndex: BattlerIndex) { + var output: string[] = []; + for (var i = 0; i < indices.length; i++) { + var selection = ""; + if (fieldIndex < 2) { + // Player + selection = targIDs[indices[i] + 1]; + } else { + // Enemy + selection = targIDs[indices[i] + 3]; + } + // If this Pokémon is on the right side of the field, flip the terms 'self' and 'ally' + if (selection == "Self" && fieldIndex % 2 == 1) { + selection = "Ally" + } + if (selection == "Ally" && fieldIndex % 2 == 1) { + selection = "Self" + } + } + return "→ " + output.join(", "); + } } diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index a43837ab1ee..0e988d75a22 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -69,16 +69,20 @@ export class SwitchPhase extends BattlePhase { this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => { if (this.isModal) {console.error("Forced Switch Detected")} if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { - const switchType = (option === PartyOption.PASS_BATON) ? SwitchType.BATON_PASS : this.switchType; + const switchType = (option === PartyOption.PASS_BATON) ? (this.switchType == SwitchType.PRE_SWITCH ? SwitchType.PRE_BATON_PASS : SwitchType.BATON_PASS) : this.switchType; switch (this.switchType) { case SwitchType.SWITCH: + case SwitchType.BATON_PASS: + // These actions have already been logged by the player; no need to set them + break; + case SwitchType.MID_TURN_SWITCH: LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Switch ${LoggerTools.playerPokeName(this.scene, fieldIndex)} to ${LoggerTools.playerPokeName(this.scene, slotIndex)}`); break; - case SwitchType.BATON_PASS: + case SwitchType.MID_TURN_BATON_PASS: LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Baton-Pass ${LoggerTools.playerPokeName(this.scene, fieldIndex)} to ${LoggerTools.playerPokeName(this.scene, slotIndex)}`); break; case SwitchType.SHED_TAIL: - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Switch to ${LoggerTools.playerPokeName(this.scene, slotIndex)}`) + LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Shed Tail to ${LoggerTools.playerPokeName(this.scene, slotIndex)}`) break; case SwitchType.PRE_SWITCH: LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Pre-Switch ${LoggerTools.playerPokeName(this.scene, fieldIndex)} to ${LoggerTools.playerPokeName(this.scene, slotIndex)}`); @@ -86,9 +90,6 @@ export class SwitchPhase extends BattlePhase { case SwitchType.PRE_BATON_PASS: LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Pre-Switch & Baton-Pass ${LoggerTools.playerPokeName(this.scene, fieldIndex)} to ${LoggerTools.playerPokeName(this.scene, slotIndex)}`); break; - default: - LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, `Switch ${LoggerTools.playerPokeName(this.scene, fieldIndex)} to ${LoggerTools.playerPokeName(this.scene, slotIndex)}`); - break; } this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, switchType, fieldIndex, slotIndex, this.doReturn)); } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index fcd56b84595..eee53f5497e 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -66,7 +66,7 @@ export class SwitchSummonPhase extends SummonPhase { const pokemon = this.getPokemon(); - if (this.switchType === SwitchType.SWITCH) { + if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.MID_TURN_SWITCH) { (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); const substitute = pokemon.getTag(SubstituteTag); if (substitute) { @@ -107,7 +107,7 @@ export class SwitchSummonPhase extends SummonPhase { const switchedInPokemon = party[this.slotIndex]; this.lastPokemon = this.getPokemon(); applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon); - if (this.switchType === SwitchType.BATON_PASS && switchedInPokemon) { + if ((this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.MID_TURN_BATON_PASS) && switchedInPokemon) { (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedInPokemon.id)); if (!this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id)) { const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier @@ -132,7 +132,7 @@ export class SwitchSummonPhase extends SummonPhase { * If this switch is passing a Substitute, make the switched Pokemon match the returned Pokemon's state as it left. * Otherwise, clear any persisting tags on the returned Pokemon. */ - if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) { + if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.MID_TURN_BATON_PASS || this.switchType === SwitchType.SHED_TAIL) { const substitute = this.lastPokemon.getTag(SubstituteTag); if (substitute) { switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; @@ -176,7 +176,7 @@ export class SwitchSummonPhase extends SummonPhase { pokemon.battleSummonData.turnCount--; } - if (this.switchType === SwitchType.BATON_PASS && pokemon) { + if ((this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.MID_TURN_BATON_PASS) && pokemon) { pokemon.transferSummon(this.lastPokemon); } else if (this.switchType === SwitchType.SHED_TAIL && pokemon) { const subTag = this.lastPokemon.getTag(SubstituteTag); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 4148f433988..cddec167db9 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -33,26 +33,8 @@ export class TurnStartPhase extends FieldPhase { 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]) - } + } else if (LoggerTools.Actions[pokemon.getBattlerIndex()].substring(0, 5) == "[???]") { + LoggerTools.Actions[pokemon.getBattlerIndex()] = mv.getName() + LoggerTools.Actions[pokemon.getBattlerIndex()].substring(5) console.log(mv.getName(), targets) } } @@ -215,6 +197,7 @@ export class TurnStartPhase extends FieldPhase { break; case Command.POKEMON: const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH; + LoggerTools.Actions.push(`${switchType == SwitchType.SWITCH ? "Switch" : "Baton-Pass"} to ${this.scene.getParty()[turnCommand.cursor!].name}`) this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, switchType, pokemon.getFieldIndex(), turnCommand.cursor!, true, pokemon.isPlayer())); break; case Command.RUN: @@ -257,10 +240,12 @@ export class TurnStartPhase extends FieldPhase { 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 } - if (LoggerTools.Actions.length > 1 && (LoggerTools.Actions[0] == "" || LoggerTools.Actions[0] == undefined || LoggerTools.Actions[0] == null)) + if (LoggerTools.Actions.length > 1 && (LoggerTools.Actions[0] == "" || LoggerTools.Actions[0] == "%SKIP" || LoggerTools.Actions[0] == undefined || LoggerTools.Actions[0] == null)) LoggerTools.Actions.shift() // If the left slot isn't doing anything, delete its entry - if (LoggerTools.Actions.length > 1 && (LoggerTools.Actions[1] == "" || LoggerTools.Actions[1] == undefined || LoggerTools.Actions[1] == null)) - LoggerTools.Actions.pop() // If the right slot isn't doing anything, delete its entry + if (LoggerTools.Actions.length > 1 && (LoggerTools.Actions[1] == "" || LoggerTools.Actions[0] == "%SKIP" || LoggerTools.Actions[1] == undefined || LoggerTools.Actions[1] == null)) + LoggerTools.Actions.pop() // If the right slot isn't doing anything, delete its entry + + // Log the player's actions LoggerTools.logActions(this.scene, this.scene.currentBattle.waveIndex, LoggerTools.Actions.join(" & ")) /** diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 31f7e08f02f..a1c092d9b9f 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -48,7 +48,7 @@ export class VictoryPhase extends PokemonPhase { this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.LOCK_CAPSULE)); } if (this.scene.currentBattle.waveIndex % 10) { - this.scene.pushPhase(new SelectModifierPhase(this.scene, undefined, undefined, undefined, undefined, this.getFixedBattleCustomModifiers())); + this.scene.pushPhase(new SelectModifierPhase(this.scene, undefined, undefined, this.getFixedBattleCustomModifiers())); } else if (this.scene.gameMode.isDaily) { LoggerTools.logShop(this.scene, this.scene.currentBattle.waveIndex, "") this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM)); diff --git a/src/test/escape-calculations.test.ts b/src/test/escape-calculations.test.ts index ecf22fc74aa..213797d60cb 100644 --- a/src/test/escape-calculations.test.ts +++ b/src/test/escape-calculations.test.ts @@ -41,7 +41,7 @@ describe("Escape chance calculations", () => { vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemySpeed]); const commandPhase = game.scene.getCurrentPhase() as CommandPhase; - commandPhase.handleCommand(Command.RUN, 0); + commandPhase.handleCommand(Command.RUN, false, 0); await game.phaseInterceptor.to(AttemptRunPhase, false); const phase = game.scene.getCurrentPhase() as AttemptRunPhase; @@ -103,7 +103,7 @@ describe("Escape chance calculations", () => { vi.spyOn(enemyField[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyBSpeed]); const commandPhase = game.scene.getCurrentPhase() as CommandPhase; - commandPhase.handleCommand(Command.RUN, 0); + commandPhase.handleCommand(Command.RUN, false, 0); await game.phaseInterceptor.to(AttemptRunPhase, false); const phase = game.scene.getCurrentPhase() as AttemptRunPhase; @@ -163,7 +163,7 @@ describe("Escape chance calculations", () => { vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemySpeed]); const commandPhase = game.scene.getCurrentPhase() as CommandPhase; - commandPhase.handleCommand(Command.RUN, 0); + commandPhase.handleCommand(Command.RUN, false, 0); await game.phaseInterceptor.to(AttemptRunPhase, false); const phase = game.scene.getCurrentPhase() as AttemptRunPhase; @@ -240,7 +240,7 @@ describe("Escape chance calculations", () => { vi.spyOn(enemyField[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyBSpeed]); const commandPhase = game.scene.getCurrentPhase() as CommandPhase; - commandPhase.handleCommand(Command.RUN, 0); + commandPhase.handleCommand(Command.RUN, false, 0); await game.phaseInterceptor.to(AttemptRunPhase, false); const phase = game.scene.getCurrentPhase() as AttemptRunPhase; diff --git a/src/test/utils/helpers/moveHelper.ts b/src/test/utils/helpers/moveHelper.ts index a53fa521785..b9b65668dbb 100644 --- a/src/test/utils/helpers/moveHelper.ts +++ b/src/test/utils/helpers/moveHelper.ts @@ -52,7 +52,7 @@ export class MoveHelper extends GameManagerHelper { this.game.scene.ui.setMode(Mode.FIGHT, (this.game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); }); this.game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { - (this.game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + (this.game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, false, movePosition, false); }); if (targetIndex !== null) { diff --git a/src/ui/ball-ui-handler.ts b/src/ui/ball-ui-handler.ts index 332fe5f65b9..bf5dc0935ee 100644 --- a/src/ui/ball-ui-handler.ts +++ b/src/ui/ball-ui-handler.ts @@ -76,7 +76,7 @@ export default class BallUiHandler extends UiHandler { success = true; if (button === Button.ACTION && this.cursor < pokeballTypeCount) { if (this.scene.pokeballCounts[this.cursor]) { - if (commandPhase.handleCommand(Command.BALL, this.cursor)) { + if (commandPhase.handleCommand(Command.BALL, false, this.cursor)) { this.scene.ui.setMode(Mode.COMMAND, commandPhase.getFieldIndex()); this.scene.ui.setMode(Mode.MESSAGE); success = true; diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 21b89bc5eba..60e091add51 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -1075,7 +1075,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { } this.currentEffectiveness = effectiveness; - if (!(this.scene as BattleScene).typeHints || effectiveness === undefined || this.flyoutMenu?.flyoutVisible) { + if (effectiveness == "" || effectiveness === undefined || this.flyoutMenu?.flyoutVisible) { this.effectivenessContainer.setVisible(false); return; } diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index 764e71a8c3f..841d724d965 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -108,7 +108,7 @@ export default class CommandUiHandler extends UiHandler { break; // Run case Command.RUN: - (this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.RUN, 0); + (this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.RUN, false, 0); success = true; break; } diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 87d98f73bd0..82bdeb43505 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -139,7 +139,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { if (button === Button.CANCEL || button === Button.ACTION) { if (button === Button.ACTION) { - if ((this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, cursor, false)) { + if ((this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, true, cursor, false)) { success = true; } else { ui.playError(); @@ -288,8 +288,14 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { } /** - * Gets multiplier text for a pokemon's move against a specific opponent - * Returns undefined if it's a status move + * Gets multiplier text for a pokemon's move against a specific opponent. + * Returns undefined if it's a status move. + * + * If Type Hints is enabled, shows the move's type effectiveness. + * + * If Damage Calculation is enabled, shows the move's expected damage range. + * + * If Type Hints and Damage Calculation are both off, the type effectiveness multiplier is hidden. */ private getEffectivenessText(pokemon: Pokemon, opponent: Pokemon, pokemonMove: PokemonMove): string | undefined { const effectiveness = opponent.getMoveEffectiveness(pokemon, pokemonMove.getMove(), !opponent.battleData?.abilityRevealed); @@ -297,9 +303,13 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { return undefined; } - return this.calcDamage(pokemon as PlayerPokemon, opponent, pokemonMove) - - return `${effectiveness}x`; + var calc = this.calcDamage(pokemon as PlayerPokemon, opponent, pokemonMove); + if (calc != "") { + if (this.scene.typeHints) return `${effectiveness}x - ${calc}`; + return calc; + } + if (this.scene.typeHints) return `${effectiveness}x`; + return ""; } displayMoves() { @@ -379,36 +389,32 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { } calcDamage(user: PlayerPokemon, target: Pokemon, move: PokemonMove) { - var dmgHigh = 0 - var dmgLow = 0 var crit = target.tryCriticalHit(user, move.getMove(), true) var out = target.getAttackDamage(user, move.getMove(), false, false, crit, true) //console.log(out) + var dmgHigh = out.damageHigh + var dmgLow = out.damageLow var minHits = 1 - var maxHits = -1 + var maxHits = -1 // If nothing changes this value, it is set to minHits 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; + 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 } } if (maxHits == -1) { @@ -417,45 +423,43 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { 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() + minHits *= h[i].getStackCount() + maxHits *= h[i].getStackCount() } } - dmgLow = dmgLow * minHits - dmgHigh = dmgHigh * maxHits + if (false) { + dmgLow = dmgLow * minHits + dmgHigh = dmgHigh * maxHits + } var qSuffix = "" if (target.isBoss()) { - var bossSegs = (target as EnemyPokemon).bossSegments - //dmgLow /= bossSegs - //dmgHigh /= bossSegs - //qSuffix = "?" + var shieldsBrokenLow = (target as EnemyPokemon).calculateBossClearedShields(dmgLow) + var shieldsBrokenHigh = (target as EnemyPokemon).calculateBossClearedShields(dmgHigh) + qSuffix = ` (${shieldsBrokenLow}-${shieldsBrokenHigh})` + if (shieldsBrokenLow == shieldsBrokenHigh) { + qSuffix = ` (${shieldsBrokenLow})` + } + dmgLow = (target as EnemyPokemon).calculateBossDamage(dmgLow); + dmgHigh = (target as EnemyPokemon).calculateBossDamage(dmgHigh); } var dmgLowP = Math.round((dmgLow)/target.getMaxHp() * 100) var dmgHighP = Math.round((dmgHigh)/target.getMaxHp() * 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)" + 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)" + koText = " " + Math.round(percentChance * 100) + "% KO" } //console.log(target.getMoveEffectiveness(user, move.getMove(), false, true) + "x - " + ((dmgLowP == dmgHighP) ? (dmgLowP + "%" + qSuffix) : (dmgLowP + "%-" + dmgHighP + "%" + qSuffix)) + koText) if (target.getMoveEffectiveness(user, move.getMove(), false, true) == undefined) { - return "---" + return "" } if (this.scene.damageDisplay == "Percent") - return target.getMoveEffectiveness(user, move.getMove(), false, true) + "x - " + (dmgLowP == dmgHighP ? dmgLowP + "%" + qSuffix : dmgLowP + "%-" + dmgHighP + "%" + qSuffix) + koText + return (dmgLowP == dmgHighP ? dmgLowP + "%" + qSuffix : dmgLowP + "%-" + dmgHighP + "%" + qSuffix) + koText if (this.scene.damageDisplay == "Value") - return target.getMoveEffectiveness(user, move.getMove(), false, true) + "x - " + (dmgLowP == dmgHighP ? dmgLowP + "%" + qSuffix : dmgLowP + "%-" + dmgHighP + "%" + qSuffix) + koText + return (dmgLow == dmgHigh ? dmgLow + qSuffix : dmgLow + "-" + dmgHigh + qSuffix) + koText //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)" : "") + return ""; } } \ No newline at end of file diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 7bc47b9bc48..9ec67297c27 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -461,7 +461,7 @@ export default class PartyUiHandler extends MessageUiHandler { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeItemTrigger, false, true); } } else if (this.cursor) { - (this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.POKEMON, this.cursor, option === PartyOption.PASS_BATON); + (this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.POKEMON, false, this.cursor, option === PartyOption.PASS_BATON); } } if (this.partyUiMode !== PartyUiMode.MODIFIER && this.partyUiMode !== PartyUiMode.TM_MODIFIER && this.partyUiMode !== PartyUiMode.MOVE_MODIFIER) {