From fd89a665102a83466170033802df9a9ef13989e7 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Wed, 14 Nov 2018 16:05:05 -0600 Subject: [PATCH] Implement new battle-text-parser All battle text messages have been moved out of `src/battle.ts` and into its own file `data/text.js`. Code for handling this is in the new files `src/battle-log.ts` and `src/battle-text-parser.ts`. `data/text.js` is now extremely self-contained, and nearly ready for translation support! This is a significant modernization of battle.ts. In addition to moving messages out: Functions for getting names (`pokemon.getLowerName()` etc) have been removed. `battle.minorQueue` has been removed. Minor lines are now processed directly on the main queue, with a new `battle.waitForAnimations` flag to decide whether or not the main queue should wait for animations to finish before moving on to the next line. `battle.waitForResult()` and `battle.endPrevAction()` have been removed. These confusingly-named functions closed the messagebar (and flush the minor queue). They've been replaced with `scene.maybeCloseMessagebar()`. `pokemon.markMove()` and `pokemon.markAbility()` have been renamed `pokemon.rememberMove()` and `pokemon.rememberAbility()`. --- .eslintignore | 1 + .gitignore | 1 + build-tools/update | 4 +- data/text.js | 1100 ++++++++ js/client-battle-tooltips.js | 8 +- js/client-battle.js | 1 - js/client-mainmenu.js | 80 + src/battle-animations-moves.ts | 47 +- src/battle-animations.ts | 146 +- src/battle-dex-data.ts | 2 +- src/battle-log.ts | 398 ++- src/battle-scene-stub.ts | 3 +- src/battle-text-parser.ts | 902 +++++++ src/battle.ts | 4429 +++++++++++--------------------- 14 files changed, 3935 insertions(+), 3187 deletions(-) create mode 100644 data/text.js create mode 100644 src/battle-text-parser.ts diff --git a/.eslintignore b/.eslintignore index 4572b0642..bc0ada612 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,6 +5,7 @@ node_modules/ /js/battle.js /js/battledata.js /js/battle-log.js +/js/battle-text-parser.js /js/battle-dex.js /js/battle-dex-data.js /js/battle-animations-moves.js diff --git a/.gitignore b/.gitignore index 60d45e047..c2086a339 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ package-lock.json /js/battle.js /js/battledata.js /js/battle-log.js +/js/battle-text-parser.js /js/battle-dex.js /js/battle-dex-data.js /js/battle-animations-moves.js diff --git a/build-tools/update b/build-tools/update index 199f0a0a8..35c7ffa73 100755 --- a/build-tools/update +++ b/build-tools/update @@ -60,7 +60,9 @@ if (!ignoreGraphics) { fs.writeFileSync( 'data/graphics.js', fs.readFileSync('js/battle-animations.js') + '\n\n' + - fs.readFileSync('js/battle-animations-moves.js') + fs.readFileSync('js/battle-animations-moves.js') + '\n\n' + + fs.readFileSync('data/text.js') + '\n\n' + + fs.readFileSync('js/battle-text-parser.js') ); } diff --git a/data/text.js b/data/text.js new file mode 100644 index 000000000..d76ff5358 --- /dev/null +++ b/data/text.js @@ -0,0 +1,1100 @@ +var BattleText = exports.BattleText = { + default: { + startBattle: "Battle started between [TRAINER] and [TRAINER]!", + winBattle: "**[TRAINER]** won the battle!", + tieBattle: "Tie between [TRAINER] and [TRAINER]!", + + pokemon: "[NICKNAME]", + opposingPokemon: "the opposing [NICKNAME]", + team: "your team", + opposingTeam: "the opposing team", + + turn: "== Turn [NUMBER] ==", + switchIn: "[TRAINER] sent out [FULLNAME]!", + switchInOwn: "Go! [FULLNAME]!", + switchOut: "[TRAINER] withdrew [NICKNAME]!", + switchOutOwn: "[NICKNAME], come back!", + drag: "[FULLNAME] was dragged out!", + faint: "[POKEMON] fainted!", + swap: "[POKEMON] and [TARGET] switched places!", + swapCenter: "[POKEMON] moved to the center!", + + zEffect: " [POKEMON] unleases its full-force Z-Move!", + move: "[POKEMON] used **[MOVE]**!", + abilityActivation: " [[POKEMON]'s [ABILITY]]", + + mega: " [POKEMON]'s [ITEM] is reacting to the Key Stone!", + megaGen6: " [POKEMON]'s [ITEM] is reacting to [TRAINER]'s Mega Bracelet!", + transformMega: "[POKEMON] has Mega Evolved into Mega [SPECIES]!", + primal: "[POKEMON]'s Primal Reversion! It reverted to its primal state!", + zPower: " [POKEMON] surrounded itself with its Z-Power!", + zBroken: " [POKEMON] couldn't fully protect itself and got hurt!", + + // in case the different default messages didn't make it obvious, the difference + // is that the `cant` message REPLACES "Pokemon used Move!", while the `fail` + // message happens AFTER "Pokemon used Move!" + cant: "[POKEMON] can't use [MOVE]!", + fail: " But it failed!", + + // n.b. this is the default message for in-battle forme changes + // for the move Transform and ability Imposter, see the entry for the move Transform + transform: "[POKEMON] transformed!", + typeChange: " [POKEMON] transformed into the [TYPE] type!", + typeChangeFromEffect: " [POKEMON]'s [EFFECT] made it the [TYPE] type!", + typeAdd: " [TYPE] type was added to [POKEMON]!", + + start: " ([POKEMON]'s [EFFECT] started!)", + end: " [POKEMON] was freed from [EFFECT]!", + activate: " ([POKEMON]'s [EFFECT] activated!)", + + changeAbility: " [POKEMON] acquired [ABILITY]!", + addItem: " [POKEMON] obtained one [ITEM].", // Trick, Switcheroo + takeItem: " [POKEMON] stole [SOURCE]'s [ITEM]!", + eatItem: " [POKEMON] ate its [ITEM]!", + useGem: " The [ITEM] strengthened [POKEMON]'s power!", + eatItemWeaken: " The [ITEM] weakened damage to [POKEMON]!", + removeItem: " [POKEMON] lost its [ITEM]!", + + damage: " ([POKEMON] was hurt!)", + damagePercentage: " ([POKEMON] lost [PERCENTAGE] of its health!)", + damageFromPokemon: " [POKEMON] is hurt by [SOURCE]'s [EFFECT]!", + damageFromItem: " [POKEMON] is hurt by its [ITEM]!", // Sticky Barb + damageFromPartialTrapping: " [POKEMON] is hurt by [MOVE]!", + heal: " [POKEMON] restored its HP.", + healFromZEffect: " [POKEMON] restored its HP using its Z-Power!", + healFromEffect: " [POKEMON] restored HP using its [EFFECT]!", + + boost: " [POKEMON]'s [STAT] rose!", + boost2: " [POKEMON]'s [STAT] rose sharply!", + boost3: " [POKEMON]'s [STAT] rose drastically!", + boost0: " [POKEMON]'s [STAT] won't go any higher!", + boostFromItem: " The [ITEM] raised [POKEMON]'s [STAT]!", + boost2FromItem: " The [ITEM] sharply raised [POKEMON]'s [STAT]!", + boost3FromItem: " The [ITEM] drastically raised [POKEMON]'s [STAT]!", + boostFromZEffect: " [POKEMON] boosted its [STAT] using its Z-Power!", + boost2FromZEffect: " [POKEMON] boosted its [STAT] sharply using its Z-Power!", + boost3FromZEffect: " [POKEMON] boosted its [STAT] drastically using its Z-Power!", + boostMultipleFromZEffect: " [POKEMON] boosted its stats using its Z-Power!", + + unboost: " [POKEMON]'s [STAT] fell!", + unboost2: " [POKEMON]'s [STAT] fell harshly!", + unboost3: " [POKEMON]'s [STAT] fell severely!", + unboost0: " [POKEMON]'s [STAT] won't go any lower!", + unboostFromItem: " The [ITEM] lowered [POKEMON]'s [STAT]!", + unboost2FromItem: " The [ITEM] harshly lowered [POKEMON]'s [STAT]!", + unboost3FromItem: " The [ITEM] drastically lowered [POKEMON]'s [STAT]!", + + swapBoost: " [POKEMON] switched stat changes with its target!", + swapOffensiveBoost: " [POKEMON] switched all changes to its Attack and Sp. Atk with its target!", + swapDefensiveBoost: " [POKEMON] switched all changes to its Defense and Sp. Def with its target!", + copyBoost: " [POKEMON] copied [TARGET]'s stat changes!", + clearBoost: " [POKEMON]'s stat changes were removed!", + clearBoostFromZEffect: " [POKEMON] returned its decreased stats to normal using its Z-Power!", + invertBoost: " [POKEMON]'s stat changes were inverted!", + clearAllBoost: " All stat changes were eliminated!", + + superEffective: " It's super effective!", + superEffectiveSpread: " It's super effective on [POKEMON]!", + resisted: " It's not very effective...", + resistedSpread: " It's not very effective on [POKEMON].", + crit: " A critical hit!", + critSpread: " A critical hit on [POKEMON]!", + immune: " It doesn't affect [POKEMON]...", + immuneNoPokemon: " It had no effect!", // old gens + immuneOHKO: " [POKEMON] is unaffected!", + miss: " [POKEMON] avoided the attack!", + missNoPokemon: " [SOURCE]'s attack missed!", // old gens + + center: " Automatic center!", + noTarget: " But there was no target...", // gen 5 and earlier + ohko: " It's a one-hit KO!", + combine: " The two moves have become one! It's a combined move!", + hitCount: " Hit [NUMBER] times!", + hitCountSingular: " Hit 1 time!", + }, + + // statuses + brn: { + start: " [POKEMON] was burned!", + startFromItem: " [POKEMON] was burned by the [ITEM]!", + alreadyStarted: " [POKEMON] already has a burn.", + end: " [POKEMON]'s burn was healed.", + endFromItem: " [POKEMON]'s [ITEM] healed its burn!", + damage: " [POKEMON] was hurt by its burn!", + }, + frz: { + start: " [POKEMON] was frozen solid!", + alreadyStarted: " [POKEMON] is already frozen solid!", + end: " [POKEMON] thawed out!", + endFromItem: " [POKEMON]'s [ITEM] defrosted it!", + endFromMove: " [POKEMON]'s [MOVE] melted the ice!", + cant: "[POKEMON] is frozen solid!", + }, + par: { + start: " [POKEMON] is paralyzed! It may be unable to move!", + alreadyStarted: " [POKEMON] is already paralyzed.", + end: " [POKEMON] was cured of paralysis.", + endFromItem: " [POKEMON]'s [ITEM] cured its paralysis!", + cant: "[POKEMON] is paralyzed! It can't move!", + }, + psn: { + start: " [POKEMON] was poisoned!", + alreadyStarted: " [POKEMON] is already poisoned.", + end: " [POKEMON] was cured of its poisoning.", + endFromItem: " [POKEMON]'s [ITEM] cured its poison!", + damage: " [POKEMON] was hurt by poison!", + }, + tox: { + start: " [POKEMON] was badly poisoned!", + startFromItem: " [POKEMON] was badly poisoned by the [ITEM]!", + end: "#psn", + endFromItem: "#psn", + alreadyStarted: "#psn", + damage: "#psn", + }, + slp: { + start: " [POKEMON] fell asleep!", + startFromRest: " [POKEMON] slept and became healthy!", + alreadyStarted: " [POKEMON] is already asleep!", + end: " [POKEMON] woke up!", + endFromItem: " [POKEMON]'s [ITEM] woke it up!", + cant: "[POKEMON] is fast asleep.", + }, + + // misc effects + confusion: { + start: " [POKEMON] became confused!", + startFromFatigue: " [POKEMON] became confused due to fatigue!", + // end: " [POKEMON] snapped out of confusion!", // PO artifact? + end: " [POKEMON] snapped out of its confusion!", + endFromItem: " [POKEMON]'s [ITEM] snapped it out of its confusion!", + alreadyStarted: " [POKEMON] is already confused!", + activate: " [POKEMON] is confused!", + damage: "It hurt itself in its confusion!", + }, + drain: { + heal: " [SOURCE] had its energy drained!", + }, + flinch: { + cant: "[POKEMON] flinched and couldn't move!", + }, + healreplacement: { + activate: " [POKEMON] will restore its replacement's HP using its Z-Power!", + }, + nopp: { + cant: "[POKEMON] used [MOVE]!\n But there was no PP left for the move!", + }, + recharge: { + cant: "[POKEMON] must recharge!", + }, + recoil: { + damage: " [POKEMON] is damaged by the recoil!", + }, + unboost: { + fail: " [POKEMON]'s stats were not lowered!", + failSingular: " [POKEMON]'s [STAT] was not lowered!", + }, + struggle: { + activate: " [POKEMON] has no moves left!", + }, + trapped: { + start: " [POKEMON] can no longer escape!", + }, + + // weather + sandstorm: { + weatherName: "Sandstorm", + start: " A sandstorm kicked up!", + startFromAbility: " [POKEMON]'s Sand Stream whipped up a sandstorm!", + end: " The sandstorm subsided.", + activate: " The sandstorm is raging.", + damage: " [POKEMON] is buffeted by the sandstorm!", + }, + sunnyday: { + weatherName: "Sun", + start: " The sunlight turned harsh!", + startFromAbility: " [POKEMON]'s Drought intensified the sun's rays!", + end: " The sunlight faded.", + activate: " (The sunlight is strong!)", + }, + raindance: { + weatherName: "Rain", + start: " It started to rain!", + startFromAbility: " [POKEMON]'s Drizzle made it rain!", + end: " The rain stopped.", + activate: " (Rain continues to fall!)", + }, + hail: { + weatherName: "Hail", + start: " It started to hail!", + startFromAbility: " [POKEMON]'s Snow Warning whipped up a hailstorm!", + end: " The hail stopped.", + activate: " The hail is crashing down.", + damage: " [POKEMON] is buffeted by the hail!", + }, + desolateland: { + weatherName: "Intense Sun", + start: " The sunlight turned extremely harsh!", + block: " The extremely harsh sunlight was not lessened at all!", + blockMove: " The Water-type attack evaporated in the harsh sunlight!", + }, + primordialsea: { + weatherName: "Heavy Rain", + start: " A heavy rain began to fall!", + end: " The heavy rain has lifted!", + block: " There is no relief from this heavy rain!", + blockMove: " The Fire-type attack fizzled out in the heavy rain!", + }, + deltastream: { + weatherName: "Strong Winds", + start: " Mysterious strong winds are protecting Flying-type Pok\u00E9mon!", + end: " The mysterious strong winds have dissipated!", + activate: " The mysterious strong winds weakened the attack!", + block: " The mysterious strong winds blow on regardless!", + }, + + // terrain + electricterrain: { + start: " An electric current runs across the battlefield!", + end: " The electricity disappeared from the battlefield.", + block: " [POKEMON] surrounds itself with electrified terrain!", + }, + grassyterrain: { + start: " Grass grew to cover the battlefield!", + end: " The grass disappeared from the battlefield.", + }, + mistyterrain: { + start: " Mist swirls around the battlefield!", + end: " The mist disappeared from the battlefield.", + block: " [POKEMON] surrounds itself with a protective mist!", + }, + psychicterrain: { + start: " The battlefield got weird!", + end: " The weirdness disappeared from the battlefield!", + block: " [POKEMON] surrounds itself with psychic terrain!", + }, + + // field effects + gravity: { + start: " Gravity intensified!", + end: " Gravity returned to normal!", + cant: "[POKEMON] can't use [MOVE] because of gravity!", + activate: "[POKEMON] couldn't stay airborne because of gravity!", + }, + magicroom: { + start: " It created a bizarre area in which Pokémon's held items lose their effects!", + end: " Magic Room wore off, and held items' effects returned to normal!", + }, + mudsport: { + start: " Electricity's power was weakened!", + end: " The effects of Mud Sport have faded.", + }, + trickroom: { + start: " [POKEMON] twisted the dimensions!", + end: " The twisted dimensions returned to normal!", + }, + watersport: { + start: " Fire's power was weakened!", + end: " The effects of Water Sport have faded.", + }, + wonderroom: { + start: " It created a bizarre area in which Defense and Sp. Def stats are swapped!", + end: " Wonder Room wore off, and Defense and Sp. Def stats returned to normal!", + }, + + // moves + afteryou: { + activate: " [TARGET] took the kind offer!", + }, + aquaring: { + start: " [POKEMON] surrounded itself with a veil of water!", + heal: " A veil of water restored [POKEMON]'s HP!", + }, + aromatherapy: { + activate: " A soothing aroma wafted through the area!", + }, + attract: { + start: " [POKEMON] fell in love!", + startFromItem: " [POKEMON] fell in love from the [ITEM]!", + end: " [POKEMON] got over its infatuation!", + endFromItem: " [POKEMON] cured its infatuation using its [ITEM]!", + activate: " [POKEMON] is in love with [TARGET]!", + cant: "[POKEMON] is immobilized by love!", + }, + auroraveil: { + start: " Aurora Veil made [TEAM] stronger against physical and special moves!", + end: " [TEAM]'s Aurora Veil wore off!", + }, + autotomize: { + start: " [POKEMON] became nimble!", + }, + beakblast: { + start: " [POKEMON] started heating up its beak!", + }, + beatup: { + activate: " [TARGET]'s attack!", // past gen only + }, + bestow: { + takeItem: " [POKEMON] received [ITEM] from [SOURCE]!", + }, + bide: { + start: " [POKEMON] is storing energy!", + end: " [POKEMON] unleashed its energy!", + activate: " [POKEMON] is storing energy!", + }, + bind: { + start: " [POKEMON] was squeezed by [SOURCE]!", + move: "#wrap", // gen 1 only + }, + brickbreak: { + activate: " [POKEMON] shattered [TEAM]'s protections!", + }, + bellydrum: { + boost: " [POKEMON] cut its own HP and maximized its Attack!" + }, + bounce: { + prepare: "[POKEMON] sprang up!", + }, + bugbite: { + removeItem: " [SOURCE] stole and ate its target's [ITEM]!", + }, + burnup: { + typeChange: " [POKEMON] burned itself out!", + }, + celebrate: { + activate: " Congratulations, [TRAINER]!", + }, + charge: { + start: " [POKEMON] began charging power!", + }, + clamp: { + start: " [SOURCE] clamped down on [POKEMON]!", + move: "#wrap", // gen 1 only + }, + craftyshield: { + start: " Crafty Shield protected [TEAM]!", + block: " Crafty Shield protected [POKEMON]!", + }, + curse: { + start: " [SOURCE] cut its own HP and put a curse on [POKEMON]!", + damage: " [POKEMON] is afflicted by the curse!", + }, + darkvoid: { + fail: "But [POKEMON] can't use the move!", + failWrongForme: "But [POKEMON] can't use it the way it is now!", + }, + destinybond: { + start: "[POKEMON] is hoping to take its attacker down with it!", + activate: " [POKEMON] took its attacker down with it!", + }, + dig: { + prepare: "[POKEMON] burrowed its way under the ground!", + }, + disable: { + start: " [POKEMON]'s [MOVE] was disabled!", + end: " [POKEMON]'s move is no longer disabled!", + }, + dive: { + prepare: "[POKEMON] hid underwater!", + }, + doomdesire: { + start: " [POKEMON] chose Doom Desire as its destiny!", + activate: " [TARGET] took the Doom Desire attack!", + }, + dragonascent: { + mega: "[TRAINER]'s fervent wish has reached [POKEMON]!\n[POKEMON] has Mega Evolved into [SPECIES]!", + }, + electrify: { + start: " [POKEMON]'s moves have been electrified!", + }, + embargo: { + start: " [POKEMON] can't use items anymore!", + end: " [POKEMON] can use items again!", + }, + encore: { + start: " [POKEMON] received an encore!", + end: " [POKEMON]'s encore ended!", + }, + endure: { + start: " [POKEMON] braced itself!", + activate: " [POKEMON] endured the hit!", + }, + fairylock: { + start: " No one will be able to run away during the next turn!", + }, + feint: { + activate: " [TARGET] fell for the feint!", + }, + firepledge: { + activate: "#waterpledge", + start: " A sea of fire enveloped [TEAM]!", + end: " The sea of fire around [TEAM] disappeared!", + damage: " [POKEMON] is hurt by the sea of fire!", + }, + firespin: { + start: " [POKEMON] became trapped in the fiery vortex!", + move: "#wrap", // gen 1 only + }, + flameburst: { + damage: " The bursting flame hit [POKEMON]!", + }, + fling: { + removeItem: " [POKEMON] flung its [ITEM]!", + }, + fly: { + prepare: "[POKEMON] flew up high!", + }, + focusenergy: { + start: " [POKEMON] is getting pumped!", + startFromItem: " [POKEMON] used the [ITEM] to get pumped!", + startFromZEffect: " [POKEMON] boosted its critical-hit ratio using its Z-Power!", + }, + focuspunch: { + start: " [POKEMON] is tightening its focus!", + cant: "[POKEMON] lost its focus and couldn't move!", + }, + followme: { + start: " [POKEMON] became the center of attention!", + startFromZEffect: " [POKEMON] became the center of attention!", + }, + foresight: { + start: " [POKEMON] was identified!", + }, + freezeshock: { + prepare: " [POKEMON] became cloaked in a freezing light!", + }, + futuresight: { + start: " [POKEMON] foresaw an attack!", + activate: " [TARGET] took the Future Sight attack!", + }, + gastroacid: { + start: " [POKEMON]'s Ability was suppressed!", + }, + geomancy: { + prepare: "[POKEMON] is absorbing power!", + }, + grasspledge: { + activate: "#waterpledge", + start: " A swamp enveloped [TEAM]!", + end: " The swamp around [TEAM] disappeared!", + }, + grudge: { + start: "[POKEMON] wants its target to bear a grudge!", + }, + guardsplit: { + activate: " [POKEMON] shared its guard with the target!", + }, + happyhour: { + activate: " Everyone is caught up in the happy atmosphere!", + }, + healbell: { + activate: " A bell chimed!", + }, + healblock: { + start: " [POKEMON] was prevented from healing!", + end: " [POKEMON]'s Heal Block wore off!", + cant: "[POKEMON] can't use [MOVE] because of Heal Block!", + }, + healingwish: { + heal: " The healing wish came true for [POKEMON]!", + }, + helpinghand: { + start: " [SOURCE] is ready to help [POKEMON]!", + }, + highjumpkick: { + damage: " [POKEMON] kept going and crashed!", + }, + hyperspacefury: { + activate: "#shadowforce", + fail: "#darkvoid", + }, + hyperspacehole: { + activate: "#shadowforce", + }, + iceburn: { + prepare: " [POKEMON] became cloaked in freezing air!", + }, + imprison: { + start: " [POKEMON] sealed any moves its target shares with it!", + cant: "[POKEMON] can't use its sealed [MOVE]!", + }, + incinerate: { + removeItem: " [POKEMON]'s [ITEM] was burned up!", + }, + infestation: { + start: " [POKEMON] has been afflicted with an infestation by [SOURCE]!", + }, + ingrain: { + start: " [POKEMON] planted its roots!", + block: " [POKEMON] anchored itself with its roots!", + heal: " [POKEMON] absorbed nutrients with its roots!", + }, + instruct: { + activate: " [TARGET] used the move instructed by [POKEMON]!", + }, + iondeluge: { + activate: " A deluge of ions showers the battlefield!", + }, + jumpkick: { + damage: "#highjumpkick", + }, + knockoff: { + removeItem: " [SOURCE] knocked off [POKEMON]'s [ITEM]!", + }, + laserfocus: { + start: " [POKEMON] concentrated intensely!", + }, + leechseed: { + start: " [POKEMON] was seeded!", + end: " [POKEMON] was freed from Leech Seed!", + damage: " [POKEMON]'s health is sapped by Leech Seed!", + }, + lightscreen: { + start: " Light Screen made [TEAM] stronger against special moves!", + end: " [TEAM]'s Light Screen wore off!", + // gen 1 + start: " [POKEMON]'s protected against special attacks!", + }, + lockon: { + start: " [SOURCE] took aim at [POKEMON]!", + }, + luckychant: { + start: " Lucky Chant shielded [TEAM] from critical hits!", + end: " [TEAM]'s Lucky Chant wore off!", + }, + lunardance: { + heal: " [POKEMON] became cloaked in mystical moonlight!", + }, + magiccoat: { + start: " [POKEMON] shrouded itself with Magic Coat!", + move: "[POKEMON] bounced the [MOVE] back!", + }, + magikarpsrevenge: { + fail: "#darkvoid", + }, + magmastorm: { + start: " [POKEMON] became trapped by swirling magma!", + }, + magnitude: { + start: " Magnitude [NUMBER]!", + }, + matblock: { + start: " [POKEMON] intends to flip up a mat and block incoming attacks!", + block: " [MOVE] was blocked by the kicked-up mat!", + }, + magnetrise: { + start: " [POKEMON] levitated with electromagnetism!", + end: " [POKEMON]'s electromagnetism wore off!", + // "The electromagnetism of [POKEMON] wore off!" // PO artifact? + }, + memento: { + heal: " [POKEMON]'s HP was restored by the Z-Power!", + }, + metronome: { + move: "Waggling a finger let it use [MOVE]!", + }, + mimic: { + start: " [POKEMON] learned [MOVE]!", + }, + mindreader: { + start: "#lockon", + }, + miracleeye: { + start: "#foresight", + }, + mist: { + start: " [TEAM] became shrouded in mist!", + end: " [TEAM] is no longer protected by mist!", + block: " [POKEMON] is protected by the mist!", + }, + naturepower: { + move: "Nature Power turned into [MOVE]!", + }, + nightmare: { + start: " [POKEMON] began having a nightmare!", + damage: " [POKEMON] is locked in a nightmare!", + }, + painsplit: { + activate: " The battlers shared their pain!", + }, + partingshot: { + heal: "#memento", + }, + payday: { + activate: " Coins were scattered everywhere!", + }, + perishsong: { + start: " All Pokémon that heard the song will faint in three turns!", + activate: " [POKEMON]'s perish count fell to [NUMBER].", + }, + phantomforce: { + prepare: "#shadowforce", + activate: "#shadowforce", + }, + pluck: { + removeItem: '#bugbite', + }, + powder: { + start: " [POKEMON] is covered in powder!", + activate: " When the flame touched the powder on the Pokémon, it exploded!", + }, + powersplit: { + activate: " [POKEMON] shared its power with the target!", + }, + powertrick: { + start: " [POKEMON] switched its Attack and Defense!", + end: '#.start', + }, + protect: { + start: " [POKEMON] protected itself!", + block: " [POKEMON] protected itself!", + }, + pursuit: { + activate: " ([TARGET] is being withdrawn...)", + }, + quash: { + activate: " [TARGET]'s move was postponed!", + }, + quickguard: { + start: " Quick Guard protected [TEAM]!", + block: " Quick Guard protected [POKEMON]!", + }, + ragepowder: { + start: '#followme', + startFromZEffect: '#followme', + }, + razorwind: { + prepare: " [POKEMON] whipped up a whirlwind!", + }, + recycle: { + addItem: " [POKEMON] found one [ITEM]!", + }, + reflect: { + start: " Reflect made [TEAM] stronger against physical moves!", + end: " [TEAM]'s Reflect wore off!", + // gen 1 + start: " [POKEMON] gained armor!", + }, + reflecttype: { + typeChange: " [POKEMON]'s type became the same as [SOURCE]'s type!", + }, + roleplay: { + changeAbility: " [POKEMON] copied [SOURCE]'s [ABILITY] Ability!", + }, + roost: { + start: " ([POKEMON] loses Flying type this turn.)", + }, + safeguard: { + start: " [TEAM] cloaked itself in a mystical veil!", + end: " [TEAM] is no longer protected by Safeguard!", + block: " [POKEMON] is protected by Safeguard!", + }, + sandtomb: { + start: " [POKEMON] became trapped by the quicksand!", + }, + shadowforce: { + activate: " It broke through [TARGET]'s protection!", + prepare: "[POKEMON] vanished instantly!", + }, + shelltrap: { + start: " [POKEMON] set a shell trap!", + prepare: " [POKEMON] set a shell trap!", + cant: "[POKEMON]'s shell trap didn't work!", + }, + sketch: { + activate: " [POKEMON] sketched [MOVE]!", + }, + skillswap: { + activate: " [POKEMON] swapped Abilities with its target!", + }, + skullbash: { + prepare: "[POKEMON] tucked in its head!", + }, + skyattack: { + prepare: "[POKEMON] became cloaked in a harsh light!", + }, + skydrop: { + prepare: "[POKEMON] took [TARGET] into the sky!", + end: " [POKEMON] was freed from the Sky Drop!", + failSelect: "Sky Drop won't let [POKEMON] go!", + failTooHeavy: " [POKEMON] is too heavy to be lifted!", + }, + smackdown: { + start: " [POKEMON] fell straight down!", + }, + snatch: { + start: " [POKEMON] waits for a target to make a move!", + activate: " [POKEMON] snatched [TARGET]'s move!", + }, + solarbeam: { + prepare: " [POKEMON] absorbed light!", + }, + solarblade: { + prepare: "#solarbeam", + }, + spectralthief: { + clearBoost: " [SOURCE] stole the target's boosted stats!", + }, + speedswap: { + activate: " [POKEMON] switched Speed with its target!", + }, + spikes: { + start: " Spikes were scattered on the ground all around [TEAM]!", + end: " The spikes disappeared from the ground around [TEAM]!", + damage: " [POKEMON] is hurt by the spikes!", + }, + spikyshield: { + damage: "#roughskin", + }, + spite: { + activate: " It reduced the PP of [TARGET]'s [MOVE] by [NUMBER]!", + }, + splash: { + activate: " But nothing happened!", + }, + spotlight: { + start: "#followme", + startFromZEffect: "#followme", + }, + stealthrock: { + start: " Pointed stones float in the air around [TEAM]!", + end: " The pointed stones disappeared from around [TEAM]!", + damage: " Pointed stones dug into [POKEMON]!", + }, + stickyweb: { + start: " A sticky web spreads out on the ground around [TEAM]!", + end: " The sticky web has disappeared from the ground around [TEAM]!", + activate: " [POKEMON] was caught in a sticky web!", + }, + stockpile: { + start: " [POKEMON] stockpiled [NUMBER]!", + end: " [POKEMON]'s stockpiled effect wore off!", + }, + substitute: { + start: " [POKEMON] put in a substitute!", + alreadyStarted: " [POKEMON] already has a substitute!", + end: " [POKEMON]'s substitute faded!", + fail: " But it does not have enough HP left to make a substitute!", + activate: " The substitute took damage for [POKEMON]!", + }, + switcheroo: { + activate: "#trick", + }, + tailwind: { + start: " The Tailwind blew from behind [TEAM]!", + end: " [TEAM]'s Tailwind petered out!", + }, + taunt: { + start: " [POKEMON] fell for the taunt!", + end: " [POKEMON]'s taunt wore off!", + cant: "[POKEMON] can't use [MOVE] after the taunt!", + }, + telekinesis: { + start: " [POKEMON] was hurled into the air!", + end: " [POKEMON] was freed from the telekinesis!", + }, + throatchop: { + cant: "The effects of Throat Chop prevent [POKEMON] from using certain moves!", + }, + torment: { + start: " [POKEMON] was subjected to torment!", + end: " [POKEMON]'s torment wore off!", + }, + toxicspikes: { + start: " Poison spikes were scattered on the ground all around [TEAM]!", + end: " The poison spikes disappeared from the ground around [TEAM]!", + }, + transform: { + transform: "[POKEMON] transformed into [SPECIES]!", + }, + trick: { + activate: " [POKEMON] switched items with its target!", + }, + uproar: { + start: " [POKEMON] caused an uproar!", + end: " [POKEMON] calmed down.", + activate: " [POKEMON] is making an uproar!", + block: " But the uproar kept [POKEMON] awake!", + blockSelf: " [POKEMON] can't sleep in an uproar!", + }, + uturn: { + switchOut: "[POKEMON] went back to [TRAINER]!", + }, + voltswitch: { + switchOut: '#uturn', + }, + waterpledge: { + activate: " [POKEMON] is waiting for [TARGET]'s move...", + start: " A rainbow appeared in the sky on [TEAM]'s side!", + end: " The rainbow on [TEAM]'s side disappeared!", + }, + weatherball: { + move: "Breakneck Blitz turned into [MOVE] due to the weather!", + }, + whirlpool: { + start: " [POKEMON] became trapped in the vortex!", + }, + wideguard: { + start: " Wide Guard protected [TEAM]!", + block: " Wide Guard protected [POKEMON]!", + }, + wish: { + heal: " [SOURCE]'s wish came true!", + }, + wrap: { + start: " [POKEMON] was wrapped by [SOURCE]!", + move: "[POKEMON]'s attack continues!", // gen 1 only + }, + yawn: { + start: " [POKEMON] grew drowsy!", + }, + + // abilities + aftermath: { + damage: " [POKEMON] is hurt!", + }, + airlock: { + start: " The effects of the weather disappeared.", + }, + angerpoint: { + boost: " [POKEMON] maxed its Attack!", + }, + anticipation: { + activate: " [POKEMON] shuddered!", + }, + aromaveil: { + block: " [TARGET] is protected by an aromatic veil!", + }, + aurabreak: { + start: " [POKEMON] reversed all other Pokémon's auras!", + }, + baddreams: { + damage: " [POKEMON] is tormented!", + }, + battlebond: { + activate: " [POKEMON] became fully charged due to its bond with its Trainer!", + transform: "[POKEMON] became Ash-Greninja!", + }, + blacksludge: { + heal: " [POKEMON] restored a little HP using its Black Sludge!", + }, + cloudnine: { + start: "#airlock", + }, + comatose: { + start: " [POKEMON] is drowsing!", + }, + damp: { + cant: "[POKEMON] cannot use [MOVE]!", + }, + darkaura: { + start: " [POKEMON] is radiating a dark aura!", + }, + dazzling: { + cant: "#damp", + }, + disguise: { + block: " Its disguise served it as a decoy!", + transform: "[POKEMON]'s disguise was busted!", + }, + dryskin: { + damage: " ([POKEMON] was hurt by its Dry Skin.)", + }, + fairyaura: { + start: " [POKEMON] is radiating a fairy aura!", + }, + flashfire: { + start: " The power of [POKEMON]'s Fire-type moves rose'!", + }, + flowerveil: { + block: " [TARGET] surrounded itself with a veil of petals!", + }, + forewarn: { + activate: " It was alerted to [TARGET]'s [MOVE]!", + activateNoTarget: " [POKEMON]'s Forewarn alerted it to [MOVE]!", + }, + frisk: { + activate: " [POKEMON] frisked [TARGET] and found its [ITEM]!", + activateNoTarget: " [POKEMON] frisked its target and found one [ITEM]!", + }, + harvest: { + addItem: " [POKEMON] harvested one [ITEM]!", + }, + illusion: { + end: " [POKEMON]'s illusion wore off!", + }, + innardsout: { + damage: "#aftermath", + }, + ironbarbs: { + damage: "#roughskin", + }, + leftovers: { + heal: " [POKEMON] restored a little HP using its Leftovers!", + }, + lightningrod: { + activate: " [POKEMON] took the attack!", + }, + liquidooze: { + damage: " [POKEMON] sucked up the liquid ooze!", + }, + magicbounce: { + move: '#magiccoat', + }, + moldbreaker: { + start: " [POKEMON] breaks the mold!", + }, + mummy: { + changeAbility: " [TARGET]'s Ability became Mummy!", + }, + naturalcure: { + activate: " ([POKEMON] is cured by its Natural Cure!)", + }, + owntempo: { + block: " [POKEMON] doesn't become confused!", + }, + persistent: { + activate: " [POKEMON] extends [MOVE] by 2 turns!", + }, + pickup: { + addItem: '#recycle', + }, + powerconstruct: { + activate: " You sense the presence of many!", + transform: "[POKEMON] transformed into its Complete Forme!", + }, + powerofalchemy: { + changeAbility: "#receiver", + }, + pressure: { + start: " [POKEMON] is exerting its pressure!", + }, + queenlymajesty: { + cant: "#damp", + }, + rebound: { + move: '#magiccoat', + }, + receiver: { + changeAbility: " [SOURCE]'s [ABILITY] was taken over!", + }, + rockyhelmet: { + damage: " [POKEMON] was hurt by the Rocky Helmet!", + }, + roughskin: { + damage: " [POKEMON] was hurt!", + }, + schooling: { + transform: "[POKEMON] formed a school!", + transformEnd: "[POKEMON] stopped schooling!", + }, + shellbell: { + heal: " [POKEMON] restored a little HP using its Shell Bell!", + }, + shieldsdown: { + // n.b. this isn't a bug, the game actually says "Shields Down deactivated" on first transformation + // https://www.youtube.com/watch?v=SThjYBz4SEA + transform: "Shields Down deactivated!\n([POKEMON] shielded itself.)", + transformEnd: "Shields Down activated!\n([POKEMON] stopped shielding itself.)", + }, + slowstart: { + start: " [POKEMON] can't get it going!", + end: " [POKEMON] finally got its act together!", + }, + solarpower: { + damage: " ([POKEMON] was hurt by its Solar Power.)", + }, + stancechange: { + transform: "Changed to Blade Forme!", + transformEnd: "Changed to Shield Forme!", + }, + stickyhold: { + block: " [POKEMON]'s item cannot be removed!", + }, + stormdrain: { + activate: "#lightningrod", + }, + sturdy: { + activate: " [POKEMON] endured the hit!", + }, + suctioncups: { + block: " [POKEMON] anchors itself!", + }, + sweetveil: { + block: " [TARGET] surrounded itself with a veil of sweetness!", + }, + symbiosis: { + activate: " [POKEMON] shared its [ITEM] with [TARGET]!", + }, + telepathy: { + block: " [POKEMON] avoids attacks by its ally Pok\u00E9mon!", + }, + teravolt: { + start: " [POKEMON] is radiating a bursting aura!", + }, + trace: { + changeAbility: " [POKEMON] traced [SOURCE]'s [ABILITY]!", + }, + truant: { + cant: "[POKEMON] is loafing around!", + }, + turboblaze: { + start: " [POKEMON] is radiating a blazing aura!", + }, + unnerve: { + start: " [TEAM] is too nervous to eat Berries!", + }, + zenmode: { + transform: 'Zen Mode triggered!', + transformEnd: 'Zen Mode ended!', + }, + + // items + airballoon: { + start: " [POKEMON] floats in the air with its Air Balloon!", + end: " [POKEMON]'s Air Balloon popped!", + }, + custapberry: { + activate: " [POKEMON]'s Custap Berry let it move first!", + }, + ejectbutton: { + end: " [POKEMON] is switched out with the Eject Button!", + }, + focusband: { + activate: " [POKEMON] hung on using its Focus Band!", + }, + focussash: { + end: " [POKEMON] hung on using its Focus Sash!", + }, + leppaberry: { + activate: " [POKEMON] restored PP to its [MOVE] move using Leppa Berry!", + }, + lifeorb: { + damage: " [POKEMON] lost some of its HP!", + }, + mysteryberry: { + activate: " [POKEMON] restored PP to its [MOVE] move using Mystery Berry!", + }, + powerherb: { + end: " [POKEMON] became fully charged due to its Power Herb!", + }, + protectivepads: { + block: " [POKEMON] protected itself with the Protective Pads!", + }, + quickclaw: { + activate: " [POKEMON]'s Quick Claw let it move first!", + }, + redcard: { + end: " [POKEMON] held up its Red Card against [TARGET]!", + }, + safetygoggles: { + block: " [POKEMON] is not affected by [MOVE] thanks to its Safety Goggles!", + }, + ultranecroziumz: { + transform: " Bright light is about to burst out of [POKEMON]!", + activate: "[POKEMON] regained its true power through Ultra Burst!", + }, + whiteherb: { + end: " [POKEMON] returned its status to normal using its White Herb!", + }, +}; diff --git a/js/client-battle-tooltips.js b/js/client-battle-tooltips.js index d4112fe8a..653b6b901 100644 --- a/js/client-battle-tooltips.js +++ b/js/client-battle-tooltips.js @@ -406,7 +406,13 @@ var BattleTooltips = (function () { var gender = pokemon.gender; if (gender) gender = ' ' + gender + ''; text = '
'; - text += '

' + pokemon.getFullName() + gender + (pokemon.level !== 100 ? ' L' + pokemon.level + '' : '') + '
'; + + var name = BattleLog.escapeHTML(pokemon.name); + if (pokemon.species !== pokemon.name) { + name += ' (' + BattleLog.escapeHTML(pokemon.species) + ')'; + } + + text += '

' + name + gender + (pokemon.level !== 100 ? ' L' + pokemon.level + '' : '') + '
'; var template = Tools.getTemplate(pokemon.getSpecies ? pokemon.getSpecies() : pokemon.species); if (pokemon.volatiles && pokemon.volatiles.formechange) { diff --git a/js/client-battle.js b/js/client-battle.js index bcd23efd4..5549e52a1 100644 --- a/js/client-battle.js +++ b/js/client-battle.js @@ -955,7 +955,6 @@ pokemonData.getFormattedRange = Pokemon.prototype.getFormattedRange; pokemonData.getHPColorClass = Pokemon.prototype.getHPColorClass; pokemonData.getHPColor = Pokemon.prototype.getHPColor; - pokemonData.getFullName = Pokemon.prototype.getFullName; } }, diff --git a/js/client-mainmenu.js b/js/client-mainmenu.js index dbf33779c..354d4bea4 100644 --- a/js/client-mainmenu.js +++ b/js/client-mainmenu.js @@ -924,6 +924,86 @@ app.addPopup(UserPopup, {name: target}); }); } + }, { + parseChatMessage: function (message, name, timestamp, isHighlighted, $chatElem) { + var showMe = !((Tools.prefs('chatformatting') || {}).hideme); + var group = ' '; + if (!/[A-Za-z0-9]/.test(name.charAt(0))) { + // Backwards compatibility + group = name.charAt(0); + name = name.substr(1); + } + var color = BattleLog.hashColor(toId(name)); + var clickableName = '' + BattleLog.escapeHTML(group) + '' + BattleLog.escapeHTML(name) + ''; + var hlClass = isHighlighted ? ' highlighted' : ''; + var mineClass = (window.app && app.user && app.user.get('name') === name ? ' mine' : ''); + + var cmd = ''; + var target = ''; + if (message.charAt(0) === '/') { + if (message.charAt(1) === '/') { + message = message.slice(1); + } else { + var spaceIndex = message.indexOf(' '); + cmd = (spaceIndex >= 0 ? message.slice(1, spaceIndex) : message.slice(1)); + if (spaceIndex >= 0) target = message.slice(spaceIndex + 1); + } + } + + switch (cmd) { + case 'me': + if (!showMe) return '
' + timestamp + '' + clickableName + ': /me' + BattleLog.parseMessage(' ' + target) + '
'; + return '
' + timestamp + ' ' + clickableName + '' + BattleLog.parseMessage(' ' + target) + '
'; + case 'mee': + if (!showMe) return '
' + timestamp + '' + clickableName + ': /me' + BattleLog.parseMessage(' ' + target).slice(1) + '
'; + return '
' + timestamp + ' ' + clickableName + '' + BattleLog.parseMessage(' ' + target).slice(1) + '
'; + case 'invite': + var roomid = toRoomid(target); + return [ + '
' + timestamp + '' + clickableName + ' invited you to join the room "' + roomid + '"
', + '
' + ]; + case 'announce': + return '
' + timestamp + '' + clickableName + ': ' + BattleLog.parseMessage(target) + '
'; + case 'log': + return '
' + timestamp + '' + BattleLog.parseMessage(target) + '
'; + case 'data-pokemon': + case 'data-item': + case 'data-ability': + case 'data-move': + return '[outdated message type not supported]'; + case 'text': + return '
' + BattleLog.parseMessage(target) + '
'; + case 'error': + return '
' + BattleLog.escapeHTML(target) + '
'; + case 'html': + return '
' + timestamp + '' + clickableName + ': ' + BattleLog.sanitizeHTML(target) + '
'; + case 'uhtml': + case 'uhtmlchange': + var parts = target.split(','); + var $elements = $chatElem.find('div.uhtml-' + toId(parts[0])); + var html = parts.slice(1).join(','); + if (!html) { + $elements.remove(); + } else if (!$elements.length) { + $chatElem.append('
' + BattleLog.sanitizeHTML(html) + '
'); + } else if (cmd === 'uhtmlchange') { + $elements.html(BattleLog.sanitizeHTML(html)); + } else { + $elements.remove(); + $chatElem.append('
' + BattleLog.sanitizeHTML(html) + '
'); + } + return ''; + case 'raw': + return '
' + BattleLog.sanitizeHTML(target) + '
'; + default: + // Not a command or unsupported. Parsed as a normal chat message. + if (!name) { + return '
' + timestamp + '' + BattleLog.parseMessage(message) + '
'; + } + return '
' + timestamp + '' + clickableName + ': ' + BattleLog.parseMessage(message) + '
'; + } + } }); var FormatPopup = this.FormatPopup = this.Popup.extend({ diff --git a/src/battle-animations-moves.ts b/src/battle-animations-moves.ts index 5a795bac4..4942241ac 100644 --- a/src/battle-animations-moves.ts +++ b/src/battle-animations-moves.ts @@ -3366,9 +3366,6 @@ const BattleMoveAnims: AnimTable = { }, 'accel'); }, prepareAnim: BattleOtherAnims.chargestatus.anim, - prepareMessage(pokemon) { - return pokemon.getName() + ' is absorbing power!'; - } }, magnetrise: { anim(scene, [attacker]) { @@ -3991,9 +3988,6 @@ const BattleMoveAnims: AnimTable = { attacker.anim({opacity: 0, time: 50}, 'linear'); scene.wait(200); }, - prepareMessage(pokemon) { - return pokemon.getName() + ' vanished instantly!'; - } }, bounce: { anim(scene, [attacker, defender]) { @@ -4074,9 +4068,6 @@ const BattleMoveAnims: AnimTable = { time: 300 }, 'linear'); }, - prepareMessage(pokemon) { - return pokemon.getName() + ' sprang up!'; - } }, dig: { anim(scene, [attacker, defender]) { @@ -4157,9 +4148,6 @@ const BattleMoveAnims: AnimTable = { time: 300 }, 'linear'); }, - prepareMessage(pokemon) { - return pokemon.getName() + ' burrowed its way under the ground!'; - } }, dive: { anim(scene, [attacker, defender]) { @@ -4238,9 +4226,6 @@ const BattleMoveAnims: AnimTable = { time: 300 }, 'swing'); }, - prepareMessage(pokemon) { - return pokemon.getName() + ' hid underwater!'; - } }, fly: { anim: BattleOtherAnims.flight.anim, @@ -4251,9 +4236,6 @@ const BattleMoveAnims: AnimTable = { time: 300 }, 'linear'); }, - prepareMessage(pokemon) { - return pokemon.getName() + ' flew up high!'; - } }, skydrop: { anim: BattleOtherAnims.contactattack.anim, @@ -4269,9 +4251,6 @@ const BattleMoveAnims: AnimTable = { time: 300 }, 'linear'); }, - prepareMessage(pokemon, pokemon2) { - return pokemon.getName() + ' took ' + pokemon2.getLowerName() + ' into the sky!'; - } }, skullbash: { anim: BattleOtherAnims.contactattack.anim, @@ -4281,9 +4260,6 @@ const BattleMoveAnims: AnimTable = { time: 300 }, 'linear'); }, - prepareMessage(pokemon) { - return pokemon.getName() + ' tucked in its head!'; - } }, skyattack: { anim: BattleOtherAnims.flight.anim, @@ -4293,9 +4269,6 @@ const BattleMoveAnims: AnimTable = { time: 300 }, 'linear'); }, - prepareMessage(pokemon) { - return pokemon.getName() + ' became cloaked in a harsh light!'; - } }, hiddenpower: { anim(scene, [attacker, defender]) { @@ -9171,9 +9144,6 @@ const BattleMoveAnims: AnimTable = { }, 'linear', 'explode'); }, prepareAnim: BattleOtherAnims.chargestatus.anim, - prepareMessage(pokemon) { - return pokemon.getName() + ' set a shell trap!'; - } }, flamecharge: { anim(scene, [attacker, defender]) { @@ -18806,9 +18776,6 @@ const BattleMoveAnims: AnimTable = { }, 'linear', 'explode'); }, prepareAnim: BattleOtherAnims.chargestatus.anim, - prepareMessage(pokemon) { - return pokemon.getName() + ' absorbed light!'; - } }, solarblade: { anim(scene, [attacker, defender]) { @@ -18932,9 +18899,6 @@ const BattleMoveAnims: AnimTable = { }, 'swing'); }, prepareAnim: BattleOtherAnims.chargestatus.anim, - prepareMessage(pokemon) { - return pokemon.getName() + ' absorbed light!'; - } }, lightofruin: { anim(scene, [attacker, defender]) { @@ -19294,9 +19258,6 @@ const BattleMoveAnims: AnimTable = { }, 'linear'); }, prepareAnim: BattleOtherAnims.selfstatus.anim, - prepareMessage(pokemon) { - return pokemon.getName() + ' became cloaked in a freezing light!'; - } }, iceburn: { anim(scene, [attacker, defender]) { @@ -19416,16 +19377,10 @@ const BattleMoveAnims: AnimTable = { }, 'linear'); }, prepareAnim: BattleOtherAnims.selfstatus.anim, - prepareMessage(pokemon) { - return pokemon.getName() + ' became cloaked in freezing air!'; - } }, razorwind: { anim: null!, prepareAnim: BattleOtherAnims.selfstatus.anim, - prepareMessage(pokemon) { - return pokemon.getName() + ' whipped up a whirlwind!'; - } }, overheat: { anim(scene, [attacker, defender]) { @@ -30711,7 +30666,7 @@ BattleMoveAnims['flatter'] = {anim:BattleMoveAnims['attract'].anim}; BattleMoveAnims['armthrust'] = {anim:BattleMoveAnims['smellingsalts'].anim, multihit:true}; -BattleMoveAnims['phantomforce'] = {anim:BattleMoveAnims['shadowforce'].anim, prepareAnim:BattleMoveAnims['shadowforce'].prepareAnim, prepareMessage:BattleMoveAnims['shadowforce'].prepareMessage}; +BattleMoveAnims['phantomforce'] = {anim:BattleMoveAnims['shadowforce'].anim, prepareAnim:BattleMoveAnims['shadowforce'].prepareAnim}; BattleMoveAnims['shadowstrike'] = {anim:BattleMoveAnims['shadowforce'].anim}; BattleMoveAnims['smackdown'] = {anim:BattleMoveAnims['rockblast'].anim}; diff --git a/src/battle-animations.ts b/src/battle-animations.ts index 14ea1f576..7320f1fe2 100644 --- a/src/battle-animations.ts +++ b/src/battle-animations.ts @@ -115,27 +115,25 @@ class BattleScene { activeCount = 1; numericId = 0; - $frame: JQuery; - $battle: JQuery = null!; - $logFrame: JQuery; - $options: JQuery = null!; - $logPreempt: JQuery = null!; - $log: JQuery = null!; - $terrain: JQuery = null!; - $weather: JQuery = null!; - $bgEffect: JQuery = null!; - $bg: JQuery = null!; - $sprite: JQuery = null!; - $sprites: [JQuery, JQuery] = [null!, null!]; - $spritesFront: [JQuery, JQuery] = [null!, null!]; - $stat: JQuery = null!; - $fx: JQuery = null!; - $leftbar: JQuery = null!; - $rightbar: JQuery = null!; - $turn: JQuery = null!; - $messagebar: JQuery = null!; - $delay: JQuery = null!; - $hiddenMessage: JQuery = null!; + $frame: JQuery; + $battle: JQuery = null!; + $options: JQuery = null!; + log: BattleLog; + $terrain: JQuery = null!; + $weather: JQuery = null!; + $bgEffect: JQuery = null!; + $bg: JQuery = null!; + $sprite: JQuery = null!; + $sprites: [JQuery, JQuery] = [null!, null!]; + $spritesFront: [JQuery, JQuery] = [null!, null!]; + $stat: JQuery = null!; + $fx: JQuery = null!; + $leftbar: JQuery = null!; + $rightbar: JQuery = null!; + $turn: JQuery = null!; + $messagebar: JQuery = null!; + $delay: JQuery = null!; + $hiddenMessage: JQuery = null!; sideConditions = [{}, {}] as [{[id: string]: Sprite[]}, {[id: string]: Sprite[]}]; @@ -146,7 +144,6 @@ class BattleScene { bgmNum = 0; preloadCache = {} as {[url: string]: HTMLImageElement}; - autoScrollOnResume = false; messagebarOpen = false; interruptionCount = 1; curWeather = ''; @@ -161,11 +158,11 @@ class BattleScene { /** jQuery objects that need to finish animating */ activeAnimations = $(); - constructor(battle: Battle, $frame: JQuery, $logFrame: JQuery) { + constructor(battle: Battle, $frame: JQuery, $logFrame: JQuery) { this.battle = battle; $frame.addClass('battle'); this.$frame = $frame; - this.$logFrame = $logFrame; + this.log = new BattleLog($logFrame[0] as HTMLDivElement, this); let numericId = 0; if (battle.id) { @@ -187,16 +184,10 @@ class BattleScene { ///////////// if (this.$options) { - this.$log.empty(); - this.$logPreempt.empty(); + this.log.reset(); } else { - this.$logFrame.empty(); this.$options = $('
'); - this.$log = $('
'); - this.$logPreempt = $('
'); - this.$logFrame.append(this.$options); - this.$logFrame.append(this.$log); - this.$logFrame.append(this.$logPreempt); + $(this.log.elem).prepend(this.$options); } // Battle frame @@ -244,7 +235,7 @@ class BattleScene { this.$battle.append(this.$hiddenMessage); if (this.battle.ignoreNicks) { - this.$log.addClass('hidenicks'); + this.log.setHideNicks(true); this.$messagebar.addClass('hidenicks'); this.$hiddenMessage.addClass('hidenicks'); } @@ -258,6 +249,8 @@ class BattleScene { this.pokemonTimeOffset = 0; this.curTerrain = ''; this.curWeather = ''; + + this.log.battleParser!.perspective = this.battle.sidesSwitched ? 1 : 0; } animationOff() { @@ -265,7 +258,6 @@ class BattleScene { this.stopAnimation(); this.animating = false; - this.autoScrollOnResume = (this.$logFrame.scrollTop()! + 60 >= this.$log.height()! + this.$logPreempt.height()! - this.$options.height()! - this.$logFrame.height()!); this.$messagebar.empty().css({ opacity: 0, height: 0 @@ -289,9 +281,6 @@ class BattleScene { } this.updateWeather(true); this.resetTurn(); - if (this.autoScrollOnResume) { - this.$logFrame.scrollTop(this.$log.height()! + this.$logPreempt.height()!); - } this.resetSideConditions(); } pause() { @@ -474,7 +463,7 @@ class BattleScene { } as JQuery.PlainObject; } - waitFor(elem: JQuery) { + waitFor(elem: JQuery) { this.activeAnimations = this.activeAnimations.add(elem); } @@ -497,24 +486,12 @@ class BattleScene { // Messagebar and log ///////////////////////////////////////////////////////////////////// - log(html: string, preempt?: boolean) { - let willScroll = false; - if (this.animating) willScroll = (this.$logFrame.scrollTop()! + 60 >= this.$log.height()! + this.$logPreempt.height()! - this.$options.height()! - this.$logFrame.height()!); - if (preempt) { - this.$logPreempt.append(html); - } else { - this.$log.append(html); - } - if (willScroll) { - this.$logFrame.scrollTop(this.$log.height()! + this.$logPreempt.height()!); - } - } preemptCatchup() { - this.$log.append(this.$logPreempt.children().first()); + this.log.preemptCatchup(); } message(message: string, hiddenMessage?: string) { if (!this.messagebarOpen) { - this.log('
'); + this.log.addSpacer(); if (this.animating) { this.$messagebar.empty(); this.$messagebar.css({ @@ -552,7 +529,14 @@ class BattleScene { this.waitFor($message); } this.messagebarOpen = true; - this.log('
' + message + (hiddenMessage ? hiddenMessage : '') + '
'); + } + maybeCloseMessagebar(args: Args, kwArgs: KWArgs) { + if (this.log.battleParser!.sectionBreak(args, kwArgs)) { + if (!this.messagebarOpen) return false; + this.closeMessagebar(); + return true; + } + return false; } closeMessagebar() { if (this.messagebarOpen) { @@ -563,17 +547,9 @@ class BattleScene { }, this.battle.messageFadeTime / this.acceleration); this.waitFor(this.$messagebar); } + return true; } - } - unlink(userid: string, showRevealButton = false) { - if (Tools.prefs('nounlink')) return; - let $messages = $('.chatmessage-' + userid); - if (!$messages.length) return; - $messages.find('a').contents().unwrap(); - if (window.BattleRoom && showRevealButton) { - $messages.hide().addClass('revealed').find('button').parent().remove(); - this.log('
'); - } + return false; } // General updating @@ -608,7 +584,6 @@ class BattleScene { const moveAnim = BattleMoveAnims[moveid]; if (!moveAnim.prepareAnim) return; moveAnim.prepareAnim(this, [attacker.sprite, defender.sprite]); - this.message('' + moveAnim.prepareMessage!(attacker, defender) + ''); } updateGen() { @@ -630,6 +605,31 @@ class BattleScene { } } + getDetailsText(pokemon: Pokemon) { + let name = pokemon.side && pokemon.side.n && (this.battle.ignoreOpponent || this.battle.ignoreNicks) ? pokemon.species : pokemon.name; + if (name !== pokemon.species) { + name += ' (' + pokemon.species + ')'; + } + if (pokemon === pokemon.side.active[0]) { + name += ' (active)'; + } else if (pokemon.fainted) { + name += ' (fainted)'; + } else { + let statustext = ''; + if (pokemon.hp !== pokemon.maxhp) { + statustext += pokemon.hpDisplay(); + } + if (pokemon.status) { + if (statustext) statustext += '|'; + statustext += pokemon.status; + } + if (statustext) { + name += ' (' + statustext + ')'; + } + } + return BattleLog.escapeHTML(name); + } + updateSidebar(side: Side) { if (!this.animating) return; let pokemonhtml = ''; @@ -648,9 +648,11 @@ class BattleScene { } else if (!poke) { pokemonhtml += ''; } else if (!poke.ident && this.battle.teamPreviewCount && this.battle.teamPreviewCount < side.pokemon.length) { - pokemonhtml += ''; + const details = this.getDetailsText(poke); + pokemonhtml += ''; } else { - pokemonhtml += ''; + const details = this.getDetailsText(poke); + pokemonhtml += ''; } if (i % 3 === 2) pokemonhtml += '

'; } @@ -729,7 +731,9 @@ class BattleScene { } side.totalPokemon = side.pokemon.length; if (textBuf) { - this.log('
' + BattleLog.escapeHTML(side.name) + '\'s team: ' + BattleLog.escapeHTML(textBuf) + '
'); + this.log.addDiv('chat battle-history', + '' + BattleLog.escapeHTML(side.name) + '\'s team: ' + BattleLog.escapeHTML(textBuf) + '' + ); } this.$sprites[siden].html(buf + buf2); @@ -1395,7 +1399,7 @@ class BattleScene { BattleSound.pauseBgm(); } destroy() { - if (this.$logFrame) this.$logFrame.empty(); + this.log.destroy(); if (this.$frame) this.$frame.empty(); this.soundStop(); this.battle = null!; @@ -1427,7 +1431,7 @@ interface InitScenePos { class Sprite { scene: BattleScene; - $el: JQuery = null!; + $el: JQuery = null!; sp: SpriteData; x: number; y: number; @@ -1493,10 +1497,10 @@ class PokemonSprite extends Sprite { cryurl: string | undefined = undefined; subsp: SpriteData | null = null; - $sub: JQuery | null = null; + $sub: JQuery | null = null; isSubActive = false; - $statbar: JQuery | null = null; + $statbar: JQuery | null = null; isBackSprite: boolean; isMissedPokemon = false; /** diff --git a/src/battle-dex-data.ts b/src/battle-dex-data.ts index 7f0892dc4..5535e8e14 100644 --- a/src/battle-dex-data.ts +++ b/src/battle-dex-data.ts @@ -141,7 +141,7 @@ const BattleStatNames = { // proper style spe: 'Spe' }; const BattleStats = { - atk: 'Attack', def: 'Defense', spa: 'Special Attack', spd: 'Special Defense', spe: 'Speed', accuracy: 'accuracy', evasion: 'evasiveness', spc: 'Special' + hp: 'HP', atk: 'Attack', def: 'Defense', spa: 'Special Attack', spd: 'Special Defense', spe: 'Speed', accuracy: 'accuracy', evasion: 'evasiveness', spc: 'Special' }; const baseSpeciesChart = [ diff --git a/src/battle-log.ts b/src/battle-log.ts index 4226b29b0..004cefeec 100644 --- a/src/battle-log.ts +++ b/src/battle-log.ts @@ -1,11 +1,296 @@ /** * Battle log - * + * + * An exercise in minimalism! This is a dependency of the client, which + * requires IE9+ and uses Preact, and the replay player, which requires + * IE7+ and uses jQuery. Therefore, this has to be compatible with IE7+ + * and use the DOM directly! + * + * Special thanks to PPK for QuirksMode.org, one of the few resources + * available for how to do web development in these conditions. + * * @author Guangcong Luo * @license MIT */ class BattleLog { + elem: HTMLDivElement; + innerElem: HTMLDivElement; + scene: BattleScene | null = null; + preemptElem: HTMLDivElement = null!; + atBottom = true; + className: string; + battleParser: BattleTextParser | null = null; + /** + * -1 = spectator: "Red sent out Pikachu!" "Blue's Eevee used Tackle!" + * 0 = player 1: "Go! Pikachu!" "The opposing Eevee used Tackle!" + * 1 = player 2: "Red sent out Pikachu!" "Eevee used Tackle!" + */ + perspective: -1 | 0 | 1 = -1; + constructor(elem: HTMLDivElement, scene?: BattleScene) { + this.elem = elem; + + elem.setAttribute('role', 'log'); + elem.innerHTML = ''; + const innerElem = document.createElement('div'); + innerElem.className = 'inner'; + elem.appendChild(innerElem); + this.innerElem = innerElem; + this.className = elem.className; + + if (scene) { + this.scene = scene; + const preemptElem = document.createElement('div'); + preemptElem.className = 'inner-preempt'; + elem.appendChild(preemptElem); + this.preemptElem = preemptElem; + this.battleParser = new BattleTextParser(); + } + + elem.onscroll = this.onScroll; + } + onScroll = () => { + const distanceFromBottom = this.elem.scrollHeight - this.elem.scrollTop - this.elem.clientHeight; + this.atBottom = (distanceFromBottom < 30); + }; + reset() { + this.innerElem.innerHTML = ''; + this.atBottom = true; + } + destroy() { + this.elem.onscroll = null; + } + setHideNicks(hideNicks: boolean) { + this.elem.className = this.className + (hideNicks ? ' hidenicks' : ''); + } + add(args: Args, kwArgs?: KWArgs, preempt?: boolean) { + if (kwArgs && kwArgs.silent) return; + let divClass = 'chat'; + let divHTML = ''; + switch (args[0]) { + case 'chat': case 'c': case 'c:': + let name, message; + let battle = this.scene && this.scene.battle; + if (args[0] === 'c:') { + name = args[2]; + message = args[3]; + } else { + name = args[1]; + message = args[2]; + } + let rank = name.charAt(0); + if (battle && battle.ignoreSpects && (rank === ' ' || rank === '+')) return; + if (battle && battle.ignoreOpponent && (rank === '\u2605' || rank === '\u2606') && toUserid(name) !== app.user.get('userid')) return; + if (window.app && app.ignore && app.ignore[toUserid(name)] && (rank === ' ' || rank === '+' || rank === '\u2605' || rank === '\u2606')) return; + let isHighlighted = window.app && app.rooms && app.rooms[battle!.roomid].getHighlight(message); + [divClass, divHTML] = this.parseChatMessage(message, name, '', isHighlighted); + if (isHighlighted) { + let notifyTitle = "Mentioned by " + name + " in " + battle!.roomid; + app.rooms[battle!.roomid].notifyOnce(notifyTitle, "\"" + message + "\"", 'highlight'); + } + break; + + case 'join': case 'j': + divHTML = '' + BattleLog.escapeHTML(args[1]) + ' joined.'; + break; + + case 'leave': case 'l': + divHTML = '' + BattleLog.escapeHTML(args[1]) + ' left.'; + break; + + case 'chatmsg': case '': + divHTML = BattleLog.escapeHTML(args[1]); + break; + + case 'chatmsg-raw': case 'raw': case 'html': + divHTML = BattleLog.sanitizeHTML(args[1]); + break; + + case 'error': case 'inactive': case 'inactiveoff': + divClass = 'chat message-error'; + divHTML = BattleLog.escapeHTML(args[1]); + break; + + case 'bigerror': + this.message('
' + BattleLog.escapeHTML(args[1]).replace(/\|/g, '
') + '
'); + return; + + case 'pm': + divHTML = '' + BattleLog.escapeHTML(args[1]) + ': (Private to ' + BattleLog.escapeHTML(args[3]) + ') ' + BattleLog.parseMessage(args[4]) + ''; + break; + + case 'askreg': + this.addDiv('chat', '
Register an account to protect your ladder rating!
'); + return; + + case 'unlink': + this.hideChatFrom(toId(args[2] || args[1])); + return; + + case 'debug': + divClass = 'debug'; + divHTML = '
[DEBUG] ' + BattleLog.escapeHTML(args[1]) + '.
'; + break; + + case 'seed': case 'choice': case ':': case 'timer': + case 'J': case 'L': case 'N': case 'n': case 'spectator': case 'spectatorleave': + return; + + default: + this.addBattleMessage(args, kwArgs); + return; + } + if (divHTML) this.addDiv(divClass, divHTML, preempt); + } + addBattleMessage(args: Args, kwArgs?: KWArgs) { + switch (args[0]) { + case 'warning': + this.message('Warning: ' + BattleLog.escapeHTML(args[1])); + this.message(`Bug? Report it to the replay viewer's Smogon thread`); + if (this.scene) this.scene.wait(1000); + return; + + case 'variation': + this.addDiv('', 'Variation: ' + BattleLog.escapeHTML(args[1]) + ''); + break; + + case 'rule': + const ruleArgs = args[1].split(': '); + this.addDiv('', '' + BattleLog.escapeHTML(ruleArgs[0]) + (ruleArgs[1] ? ':' : '') + ' ' + BattleLog.escapeHTML(ruleArgs[1] || '') + ''); + break; + + case 'rated': + this.addDiv('rated', '' + (BattleLog.escapeHTML(args[1]) || 'Rated battle') + ''); + break; + + case 'tier': + this.addDiv('', 'Format:
' + BattleLog.escapeHTML(args[1]) + ''); + break; + + case 'turn': + const h2elem = document.createElement('h2'); + h2elem.className = 'battle-history'; + let turnMessage = this.battleParser!.parseLine(args, {}).trim(); + if (!turnMessage.startsWith('==') || !turnMessage.endsWith('==')) { + throw new Error("Turn message must be a heading."); + } + turnMessage = turnMessage.slice(2, -2).trim(); + this.battleParser!.curLineSection = 'break'; + h2elem.innerHTML = BattleLog.escapeHTML(turnMessage); + this.addNode(h2elem); + break; + + default: + let line = null; + if (this.battleParser) { + line = this.battleParser.parseLine(args, kwArgs || {}, true); + } + if (line === null) { + this.addDiv('chat message-error', 'Unrecognized: |' + BattleLog.escapeHTML(args.join('|'))); + return; + } + if (!line) return; + this.message(...this.parseLogMessage(line)); + break; + } + } + /** + * To avoid trolling with nicknames, we can't just run this through + * parseMessage + */ + parseLogMessage(message: string): [string, string] { + const messages = message.split('\n').map(line => { + line = BattleLog.escapeHTML(line); + line = line.replace(/\*\*(.*)\*\*/, '$1'); + line = line.replace(/\|\|([^\|]*)\|\|([^\|]*)\|\|/, '$2'); + if (line.startsWith(' ')) line = '' + line.trim() + ''; + return line; + }); + return [ + messages.join('
'), + messages.filter(message => !message.startsWith('[')).join('
'), + ]; + } + message(message: string, sceneMessage = message) { + if (this.scene) this.scene.message(sceneMessage); + this.addDiv('battle-history', message); + } + addNode(node: HTMLElement, preempt?: boolean) { + (preempt ? this.preemptElem : this.innerElem).appendChild(node); + if (this.atBottom) { + this.elem.scrollTop = this.elem.scrollHeight; + } + } + addDiv(className: string, innerHTML: string, preempt?: boolean) { + const el = document.createElement('div'); + el.className = className; + el.innerHTML = innerHTML; + this.addNode(el); + } + addSpacer() { + this.addDiv('spacer battle-history', ''); + } + changeUhtml(id: string, html: string, forceAdd?: boolean) { + id = toId(id); + const classContains = ' uhtml-' + id + ' '; + let elements = [] as HTMLDivElement[]; + for (const node of this.innerElem.childNodes as any) { + if (node.className && (' ' + node.className + ' ').includes(classContains)) { + elements.push(node); + } + } + for (const node of this.preemptElem.childNodes as any) { + if (node.className && (' ' + node.className + ' ').includes(classContains)) { + elements.push(node); + } + } + if (html && elements.length && !forceAdd) { + for (const element of elements) { + element.innerHTML = BattleLog.sanitizeHTML(html); + } + return; + } + for (const element of elements) { + element.parentElement!.removeChild(element); + } + if (html) { + this.addDiv('notice uhtml-' + id, BattleLog.sanitizeHTML(html)); + } + } + hideChatFrom(userid: ID, showRevealButton = true) { + const classStart = 'chat chatmessage-' + userid + ' '; + let lastNode; + let count = 0; + for (const node of this.innerElem.childNodes as any) { + if (node.className && (node.className + ' ').startsWith(classStart)) { + node.style.display = 'none'; + node.className = 'revealed ' + node.className; + count++; + } + lastNode = node; + } + for (const node of this.preemptElem.childNodes as any) { + if (node.className && (node.className + ' ').startsWith(classStart)) { + node.style.display = 'none'; + node.className = 'revealed ' + node.className; + count++; + } + lastNode = node; + } + if (!count || !showRevealButton) return; + const button = document.createElement('button'); + button.name = 'toggleMessages'; + button.value = userid; + button.className = 'subtle'; + button.innerHTML = '(' + count + ' line' + (count > 1 ? 's' : '') + ' from ' + userid + ' hidden)'; + lastNode.appendChild(document.createTextNode(' ')); + lastNode.appendChild(button); + } + preemptCatchup() { + if (!this.preemptElem.firstChild) return; + this.innerElem.appendChild(this.preemptElem.firstChild); + } + static escapeFormat(formatid: string): string { let atIndex = formatid.indexOf('@@@'); if (atIndex >= 0) { @@ -77,16 +362,24 @@ class BattleLog { return this.colorCache[name]; } - static parseChatMessage(message: string, name: string, timestamp: string, isHighlighted?: boolean, $chatElem?: any) { - let showMe = !((Tools.prefs('chatformatting') || {}).hideme); + static prefs(name: string) { + // @ts-ignore + if (window.Storage && Storage.prefs) return Storage.prefs(name); + // @ts-ignore + if (window.PS) return PS.prefs[name]; + return undefined; + } + + parseChatMessage(message: string, name: string, timestamp: string, isHighlighted?: boolean) { + let showMe = !(BattleLog.prefs('chatformatting') || {}).hideme; let group = ' '; if (!/[A-Za-z0-9]/.test(name.charAt(0))) { // Backwards compatibility group = name.charAt(0); name = name.substr(1); } - let color = this.hashColor(toId(name)); - let clickableName = '' + this.escapeHTML(group) + '' + this.escapeHTML(name) + ''; + let color = BattleLog.hashColor(toId(name)); + let clickableName = '' + BattleLog.escapeHTML(group) + '' + BattleLog.escapeHTML(name) + ''; let hlClass = isHighlighted ? ' highlighted' : ''; let mineClass = (window.app && app.user && app.user.get('name') === name ? ' mine' : ''); @@ -104,99 +397,44 @@ class BattleLog { switch (cmd) { case 'me': - if (!showMe) return '
' + timestamp + '' + clickableName + ': /me' + this.parseMessage(' ' + target) + '
'; - return '
' + timestamp + ' ' + clickableName + '' + this.parseMessage(' ' + target) + '
'; + if (!showMe) return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '' + clickableName + ': /me' + BattleLog.parseMessage(' ' + target) + '']; + return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + ' ' + clickableName + '' + BattleLog.parseMessage(' ' + target) + '']; case 'mee': - if (!showMe) return '
' + timestamp + '' + clickableName + ': /me' + this.parseMessage(' ' + target).slice(1) + '
'; - return '
' + timestamp + ' ' + clickableName + '' + this.parseMessage(' ' + target).slice(1) + '
'; + if (!showMe) return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '' + clickableName + ': /me' + BattleLog.parseMessage(' ' + target).slice(1) + '']; + return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + ' ' + clickableName + '' + BattleLog.parseMessage(' ' + target).slice(1) + '']; case 'invite': let roomid = toRoomid(target); - return [ - '
' + timestamp + '' + clickableName + ' invited you to join the room "' + roomid + '"
', - '
' - ]; + return ['chat', timestamp + '' + clickableName + ' invited you to join the room "' + roomid + '"' + + '
']; case 'announce': - return '
' + timestamp + '' + clickableName + ': ' + this.parseMessage(target) + '
'; + return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '' + clickableName + ': ' + BattleLog.parseMessage(target) + '']; case 'log': - return '
' + timestamp + '' + this.parseMessage(target) + '
'; + return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '' + BattleLog.parseMessage(target) + '']; case 'data-pokemon': - let buf = '
  • '; - let template = Tools.getTemplate(target); - if (!template.abilities || !template.baseStats) return '[not supported in replays]'; - buf += '' + (template.tier || Tools.getTemplate(template.baseSpecies).tier) + ' '; - buf += ' '; - buf += '' + template.species + ' '; - buf += ''; - if (template.types) for (let i = 0; i < template.types.length; i++) { - buf += Tools.getTypeIcon(template.types[i]); - } - buf += ' '; - buf += ''; - if (template.abilities['1']) { - buf += '' + template.abilities['0'] + '
    ' + template.abilities['1'] + '
    '; - } else { - buf += '' + template.abilities['0'] + ''; - } - if (template.abilities['S']) { - buf += '' + template.abilities['H'] + '
    ' + template.abilities['S'] + '
    '; - } else if (template.abilities['H']) { - buf += '' + template.abilities['H'] + ''; - } else { - buf += ''; - } - buf += '
    '; - buf += ''; - buf += 'HP
    ' + template.baseStats.hp + '
    '; - buf += 'Atk
    ' + template.baseStats.atk + '
    '; - buf += 'Def
    ' + template.baseStats.def + '
    '; - buf += 'SpA
    ' + template.baseStats.spa + '
    '; - buf += 'SpD
    ' + template.baseStats.spd + '
    '; - buf += 'Spe
    ' + template.baseStats.spe + '
    '; - let bst = 0; - for (const i in template.baseStats) bst += template.baseStats[i as StatName]; - buf += 'BST
    ' + bst + '
    '; - buf += '
    '; - buf += '
  • '; - return '
      ' + buf + '
    '; case 'data-item': - if (!window.BattleSearch) return '[not supported in replays]'; - return '
      ' + BattleSearch.renderItemRow(Tools.getItem(target), 0, 0) + '
    '; case 'data-ability': - if (!window.BattleSearch) return '[not supported in replays]'; - return '
      ' + BattleSearch.renderAbilityRow(Tools.getAbility(target), 0, 0) + '
    '; case 'data-move': - if (!window.BattleSearch) return '[not supported in replays]'; - return '
      ' + BattleSearch.renderMoveRow(Tools.getMove(target), 0, 0) + '
    '; + return ['chat message-error', '[outdated code no longer supported]']; case 'text': - return '
    ' + this.parseMessage(target) + '
    '; + return ['chat', BattleLog.parseMessage(target)]; case 'error': - return '
    ' + this.escapeHTML(target) + '
    '; + return ['chat message-error', BattleLog.escapeHTML(target)]; case 'html': - return '
    ' + timestamp + '' + clickableName + ': ' + BattleLog.sanitizeHTML(target) + '
    '; + return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '' + clickableName + ': ' + BattleLog.sanitizeHTML(target) + '']; case 'uhtml': case 'uhtmlchange': let parts = target.split(','); - let $elements = $chatElem.find('div.uhtml-' + toId(parts[0])); - let html = parts.slice(1).join(','); - if (!html) { - $elements.remove(); - } else if (!$elements.length) { - $chatElem.append('
    ' + BattleLog.sanitizeHTML(html) + '
    '); - } else if (cmd === 'uhtmlchange') { - $elements.html(BattleLog.sanitizeHTML(html)); - } else { - $elements.remove(); - $chatElem.append('
    ' + BattleLog.sanitizeHTML(html) + '
    '); - } - return ''; + let html = parts.slice(1).join(',').trim(); + this.changeUhtml(parts[0], html, cmd === 'uhtml'); + return ['', '']; case 'raw': - return '
    ' + BattleLog.sanitizeHTML(target) + '
    '; + return ['chat', BattleLog.sanitizeHTML(target)]; default: // Not a command or unsupported. Parsed as a normal chat message. if (!name) { - return '
    ' + timestamp + '' + this.parseMessage(message) + '
    '; + return ['chat' + hlClass, timestamp + '' + BattleLog.parseMessage(message) + '']; } - return '
    ' + timestamp + '' + clickableName + ': ' + this.parseMessage(message) + '
    '; + return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '' + clickableName + ': ' + BattleLog.parseMessage(message) + '']; } } @@ -207,7 +445,7 @@ class BattleLog { if (str.substr(0, 3) === '<< ') return this.escapeHTML(str); str = formatText(str); - let options = Tools.prefs('chatformatting') || {}; + let options = BattleLog.prefs('chatformatting') || {}; if (options.hidelinks) { str = str.replace(/]*>/g, '').replace(/<\/a>/g, ''); @@ -484,3 +722,5 @@ class BattleLog { return 'data:text/plain;base64,' + encodeURIComponent(btoa(unescape(encodeURIComponent(this.createReplayFile(room))))); } } + +exports.BattleLog = BattleLog; diff --git a/src/battle-scene-stub.ts b/src/battle-scene-stub.ts index 0f2f947bf..1ff858d3a 100644 --- a/src/battle-scene-stub.ts +++ b/src/battle-scene-stub.ts @@ -7,12 +7,14 @@ class BattleSceneStub { timeOffset: number = NaN; interruptionCount: number = NaN; messagebarOpen: boolean = false; + log = {add: (args: Args, kwargs?: KWArgs) => {}} as BattleLog; abilityActivateAnim(pokemon: Pokemon, result: string): void { } addPokemonSprite(pokemon: Pokemon) { return null!; } addSideCondition(siden: number, id: ID, instant?: boolean | undefined): void { } animationOff(): void { } animationOn(): void { } + maybeCloseMessagebar(args: Args, kwArgs: KWArgs): boolean { return false; } closeMessagebar(): void { } damageAnim(pokemon: Pokemon, damage: string | number): void { } destroy(): void { } @@ -20,7 +22,6 @@ class BattleSceneStub { healAnim(pokemon: Pokemon, damage: string | number): void { } hideJoinButtons(): void { } incrementTurn(): void { } - log(html: string, preempt?: boolean | undefined): void { } message(message: string, hiddenMessage?: string | undefined): void { } pause(): void { } preemptCatchup(): void { } diff --git a/src/battle-text-parser.ts b/src/battle-text-parser.ts new file mode 100644 index 000000000..c4dea5988 --- /dev/null +++ b/src/battle-text-parser.ts @@ -0,0 +1,902 @@ +declare const BattleText: {[id: string]: {[templateName: string]: string}}; + +type Args = [string, ...string[]]; +type KWArgs = {[kw: string]: string}; + +class BattleTextParser { + p1 = "Player 1"; + p2 = "Player 2"; + perspective: 0 | 1; + gen = 7; + curLineSection: 'break' | 'preMajor' | 'major' | 'postMajor' = 'break'; + lowercaseRegExp: RegExp | null | undefined = undefined; + + constructor(perspective: 0 | 1 = 0) { + this.perspective = perspective; + } + + fixLowercase(input: string) { + if (this.lowercaseRegExp === undefined) { + const prefixes = ['pokemon', 'opposingPokemon', 'team', 'opposingTeam'].map(templateId => { + const template = BattleText.default[templateId]; + if (template.charAt(0) === template.charAt(0).toUpperCase()) return ''; + const bracketIndex = template.indexOf('['); + if (bracketIndex >= 0) return template.slice(0, bracketIndex); + return template; + }).filter(prefix => prefix); + if (prefixes.length) { + let buf = `((?:^|\n)(?: | \\\(| \\\[)?)(` + + prefixes.map(BattleTextParser.escapeRegExp).join('|') + + `)`; + this.lowercaseRegExp = new RegExp(buf, 'g'); + } else { + this.lowercaseRegExp = null; + } + } + if (!this.lowercaseRegExp) return input; + return input.replace(this.lowercaseRegExp, (match, p1, p2) => ( + p1 + p2.charAt(0).toUpperCase() + p2.slice(1) + )); + } + + static escapeRegExp(input: string) { + return input.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); + } + + pokemon(pokemon: string) { + if (!pokemon) return ''; + let side; + if (pokemon.startsWith('p1')) side = 0; + else if (pokemon.startsWith('p2')) side = 1; + else return '???pokemon:' + pokemon + '???'; + if (pokemon.charAt(3) === ':') pokemon = pokemon.slice(4).trim(); + else if (pokemon.charAt(2) === ':') pokemon = pokemon.slice(3).trim(); + else return '???pokemon:' + pokemon + '???'; + let template = BattleText.default[side === this.perspective ? 'pokemon' : 'opposingPokemon']; + return template.replace('[NICKNAME]', pokemon); + } + + pokemonName(pokemon: string) { + if (!pokemon) return ''; + if (!pokemon.startsWith('p1') && !pokemon.startsWith('p2')) return '???pokemon:' + pokemon + '???'; + if (pokemon.charAt(3) === ':') pokemon = pokemon.slice(4).trim(); + else if (pokemon.charAt(2) === ':') pokemon = pokemon.slice(3).trim(); + else return '???pokemon:' + pokemon + '???'; + return pokemon; + } + + pokemonFull(pokemon: string, details: string) { + if (pokemon.startsWith('p1')) {} + else if (pokemon.startsWith('p2')) {} + else return '???pokemon:' + pokemon + '???'; + let nickname; + if (pokemon.charAt(3) === ':') nickname = pokemon.slice(4).trim(); + else if (pokemon.charAt(2) === ':') nickname = pokemon.slice(3).trim(); + else return '???pokemon:' + pokemon + '???'; + + let species = details.split(',')[0]; + if (nickname === species) return [pokemon.slice(0, 2), `**${species}**`]; + return [pokemon.slice(0, 2), `${nickname} (**${species}**)`]; + } + + trainer(side: string) { + if (side === 'p1') return this.p1; + if (side === 'p2') return this.p2; + return '???side:' + side + '???'; + } + + team(side: string) { + if ((side === 'p1' && this.perspective === 0) || + (side === 'p2' && this.perspective === 1)) { + return BattleText.default.team; + } + return BattleText.default.opposingTeam; + } + + own(side: string) { + if (side === 'p1' && this.perspective === 0) return 'OWN'; + if (side === 'p2' && this.perspective === 1) return 'OWN'; + return ''; + } + + effectId(effect?: string) { + if (!effect) return ''; + if (effect.startsWith('item:') || effect.startsWith('move:')) { + effect = effect.slice(5); + } else if (effect.startsWith('ability:')) { + effect = effect.slice(8); + } + return toId(effect); + } + + effect(effect?: string) { + if (!effect) return ''; + if (effect.startsWith('item:') || effect.startsWith('move:')) { + effect = effect.slice(5); + } else if (effect.startsWith('ability:')) { + effect = effect.slice(8); + } + return effect.trim(); + } + + template(type: string, ...namespaces: (string | undefined)[]) { + for (const namespace of namespaces) { + if (!namespace) continue; + if (namespace === 'OWN') { + return BattleText.default[type + 'Own'] + '\n'; + } + if (namespace === 'NODEFAULT') { + return ''; + } + let id = this.effectId(namespace); + if (BattleText[id] && BattleText[id][type]) { + if (BattleText[id][type].charAt(1) === '.') type = BattleText[id][type].slice(2) as ID; + if (BattleText[id][type].charAt(0) === '#') id = BattleText[id][type].slice(1) as ID; + return BattleText[id][type] + '\n'; + } + } + if (!BattleText.default[type]) return ''; + return BattleText.default[type] + '\n'; + } + + maybeAbility(effect: string | undefined, holder: string) { + if (!effect) return ''; + if (!effect.startsWith('ability:')) return ''; + return this.ability(effect.slice(8).trim(), holder); + } + + ability(name: string | undefined, holder: string) { + if (!name) return ''; + return BattleText.default.abilityActivation.replace('[POKEMON]', this.pokemon(holder)).replace('[ABILITY]', this.effect(name)) + '\n'; + } + + lineSection(args: Args, kwArgs: KWArgs) { + const cmd = args[0]; + switch (cmd) { + case 'done' : case 'turn': + return 'break'; + case 'move' : case 'cant': case 'switch': case 'drag': case 'upkeep': case 'start': case '-mega': + return 'major'; + case 'switchout': case 'faint': + return 'preMajor'; + case '-zpower': + return 'postMajor'; + case '-damage': { + const id = this.effectId(kwArgs.from); + if (id === 'confusion') return 'major'; + } + case '-curestatus': { + const id = this.effectId(kwArgs.from); + if (id === 'naturalcure') return 'preMajor'; + } + case '-start': { + const id = this.effectId(kwArgs.from); + if (id === 'protean') return 'preMajor'; + } + case '-activate': { + const id = this.effectId(kwArgs.from); + if (id === 'confusion' || id === 'attract') return 'preMajor'; + } + } + return (cmd.charAt(0) === '-' ? 'postMajor' : ''); + } + + sectionBreak(args: Args, kwArgs: KWArgs) { + const prevSection = this.curLineSection; + const curSection = this.lineSection(args, kwArgs); + if (!curSection) return false; + this.curLineSection = curSection; + switch (curSection) { + case 'break': + if (prevSection !== 'break') return true; + return false; + case 'preMajor': + case 'major': + if (prevSection === 'postMajor' || prevSection === 'major') return true; + return false; + case 'postMajor': + return false; + } + } + + parseLine(args: Args, kwArgs: KWArgs, noSectionBreak?: boolean) { + let buf = !noSectionBreak && this.sectionBreak(args, kwArgs) ? '\n' : ''; + return buf + this.fixLowercase(this.parseLineInner(args, kwArgs) || ''); + } + + parseLineInner(args: Args, kwArgs: KWArgs) { + let cmd = args[0]; + switch (cmd) { + case 'player': { + const [, side, name] = args; + if (side === 'p1' && name) { + this.p1 = name; + } else if (side === 'p2' && name) { + this.p2 = name; + } + return ''; + } + + case 'gen': { + const [, number] = args; + this.gen = parseInt(number, 10); + return ''; + } + + case 'turn': { + const [, number] = args; + return this.template('turn').replace('[NUMBER]', number) + '\n'; + } + + case 'start': { + return this.template('startBattle').replace('[TRAINER]', this.p1).replace('[TRAINER]', this.p2); + } + + case 'win': case 'tie': { + const [, name] = args; + if (cmd === 'tie' || !name) { + return this.template('tieBattle').replace('[TRAINER]', this.p1).replace('[TRAINER]', this.p2); + } + return this.template('winBattle').replace('[TRAINER]', name); + } + + case 'switch': { + const [, pokemon, details] = args; + const [side, fullname] = this.pokemonFull(pokemon, details); + const template = this.template('switchIn', this.own(side)); + return template.replace('[TRAINER]', this.trainer(side)).replace('[FULLNAME]', fullname); + } + + case 'drag': { + const [, pokemon, details] = args; + const [side, fullname] = this.pokemonFull(pokemon, details); + const template = this.template('drag'); + return template.replace('[TRAINER]', this.trainer(side)).replace('[FULLNAME]', fullname); + } + + case 'detailschange': case '-transform': case '-formechange': { + const [, pokemon, arg2, arg3] = args; + let newSpecies = ''; + switch (cmd) { + case 'detailschange': newSpecies = arg2.split(',')[0].trim(); break; + case '-transform': newSpecies = arg3; break; + case '-formechange': newSpecies = arg2; break; + } + let newSpeciesId = toId(newSpecies); + let id = ''; + let templateName = 'transform'; + if (cmd !== '-transform') { + switch (newSpeciesId) { + case 'greninjaash': id = 'battlebond'; break; + case 'mimikyubusted': id = 'disguise'; break; + case 'zygardecomplete': id = 'powerconstruct'; break; + case 'necrozmaultra': id = 'ultranecroziumz'; break; + case 'darmanitanzen': id = 'zenmode'; break; + case 'darmanitan': id = 'zenmode'; templateName = 'transformEnd'; break; + case 'aegislashblade': id = 'stancechange'; break; + case 'aegislash': id = 'stancechange'; templateName = 'transformEnd'; break; + case 'wishiwashischool': id = 'schooling'; break; + case 'wishiwashi': id = 'schooling'; templateName = 'transformEnd'; break; + case 'miniormeteor': id = 'shieldsdown'; break; + case 'minior': id = 'shieldsdown'; templateName = 'transformEnd'; break; + } + } else if (newSpecies) { + id = 'transform'; + } + const template = this.template(templateName, id, kwArgs.msg ? '' : 'NODEFAULT'); + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SPECIES]', newSpecies); + } + + case 'switchout': { + const [, pokemon] = args; + const side = pokemon.slice(0, 2); + const template = this.template('switchOut', kwArgs.from, this.own(side)); + return template.replace('[TRAINER]', this.trainer(side)).replace('[NICKNAME]', this.pokemonName(pokemon)); + } + + case 'faint': { + const [, pokemon] = args; + const template = this.template('faint'); + return template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case 'swap': { + const [, pokemon, target] = args; + if (!target || !isNaN(Number(target))) { + const template = this.template('swapCenter'); + return template.replace('[POKEMON]', this.pokemon(pokemon)); + } + const template = this.template('swap'); + return template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target)); + } + + case 'move': { + const [, pokemon, move] = args; + let line1 = ''; + if (kwArgs.zEffect) { + line1 = this.template('zEffect').replace('[POKEMON]', this.pokemon(pokemon)); + } + const template = this.template('move', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', move); + } + + case 'cant': { + let [, pokemon, effect, move] = args; + let id = this.effectId(effect); + switch (id) { + case 'damp': case 'dazzling': case 'queenlymajesty': + // thanks Marty + [pokemon, kwArgs.of] = [kwArgs.of, pokemon]; + break; + } + const template = this.template('cant', effect); + const line1 = this.maybeAbility(effect, kwArgs.of || pokemon); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', move); + } + + case 'faint': { + const [, pokemon] = args; + const template = this.template('faint'); + return template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case 'message': { + let [, message] = args; + return '' + message + '\n'; + } + + case '-start': { + let [, pokemon, effect, arg3] = args; + const line1 = this.maybeAbility(effect, pokemon) || this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + let id = this.effectId(effect); + if (id === 'typechange') { + const template = this.template('typeChange', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TYPE]', arg3).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + if (id === 'typeadd') { + const template = this.template('typeAdd', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TYPE]', arg3); + } + if (id.startsWith('stockpile')) { + const number = id.slice(9); + const template = this.template('start', 'stockpile'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[NUMBER]', number); + } + if (id.startsWith('perish')) { + const number = id.slice(6); + const template = this.template('activate', 'perishsong'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[NUMBER]', number); + } + let templateId = 'start'; + if (kwArgs.already) templateId = 'alreadyStarted'; + if (kwArgs.fatigue) templateId = 'startFromFatigue'; + if (kwArgs.zeffect) templateId = 'startFromZEffect'; + if (kwArgs.damage) templateId = 'activate'; + if (kwArgs.block) templateId = 'block'; + if (kwArgs.upkeep) templateId = 'activate'; + let template = ''; + if (templateId === 'start' && kwArgs.from && kwArgs.from.startsWith('item:')) { + template = this.template('startFromItem', effect); + } + if (!template) template = this.template(templateId, effect); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(kwArgs.from)).replace('[MOVE]', arg3).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + + case '-end': { + let [, pokemon, effect] = args; + const line1 = this.maybeAbility(effect, pokemon) || this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + let id = this.effectId(effect); + if (id === 'doomdesire' || id === 'futuresight') { + const template = this.template('activate', effect); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(kwArgs.of)); + } + let templateId = 'end'; + let template = ''; + if (kwArgs.from && kwArgs.from.startsWith('item:')) { + template = this.template('endFromItem', effect); + } + if (!template) template = this.template(templateId, effect); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(kwArgs.from)).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + + case '-ability': { + let [, pokemon, ability, oldAbility, arg4] = args; + let line1 = ''; + if (oldAbility && (oldAbility.startsWith('p1') || oldAbility.startsWith('p2') || oldAbility === 'boost')) { + arg4 = oldAbility; + oldAbility = ''; + } + if (oldAbility) line1 += this.ability(oldAbility, pokemon); + line1 += this.ability(ability, pokemon); + if (kwArgs.fail) { + const template = this.template('block', ability); + return line1 + template; + } + if (kwArgs.from) { + const template = this.template('changeAbility', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ABILITY]', this.effect(ability)).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + const id = this.effectId(ability); + if (id === 'unnerve') { + const template = this.template('start', ability); + return line1 + template.replace('[TEAM]', this.team(arg4 && arg4.slice(0, 2))); + } + let templateId = 'start'; + if (id === 'anticipation' || id === 'sturdy') templateId = 'activate'; + const template = this.template(templateId, ability, 'NODEFAULT'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-endability': { + let [, pokemon, ability] = args; + if (ability) return this.ability(ability, pokemon); + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + const template = this.template('start', 'Gastro Acid'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-item': { + let [, pokemon, item] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + const id = this.effectId(kwArgs.from); + if (['thief', 'covet', 'bestow'].includes(id)) { + const template = this.template('takeItem', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + if (id === 'frisk') { + const template = this.template(kwArgs.of && pokemon && kwArgs.of !== pokemon ? 'activate' : 'activateNoTarget', "Frisk"); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)).replace('[TARGET]', this.pokemon(kwArgs.of)); + } + if (kwArgs.from) { + const template = this.template('addItem', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)); + } + const template = this.template('start', item, 'NODEFAULT'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-enditem': { + let [, pokemon, item, arg3] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + if (kwArgs.eat) { + const template = this.template('eatItem', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)); + } + const id = this.effectId(kwArgs.from); + if (id === 'gem') { + const template = this.template('useGem', item); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)).replace('[MOVE]', kwArgs.move); + } + if (id === 'stealeat') { + const template = this.template('removeItem', "Bug Bite"); + return line1 + template.replace('[SOURCE]', this.pokemon(kwArgs.of)).replace('[ITEM]', this.effect(item)); + } + if (kwArgs.from) { + const template = this.template('removeItem', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + const template = this.template('end', item, 'NODEFAULT'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(kwArgs.of)); + } + + case '-status': { + const [, pokemon, status] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + const template = this.template('start', status); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-curestatus': { + const [, pokemon, status] = args; + if (this.effectId(kwArgs.from) === 'naturalcure') { + const template = this.template('activate', kwArgs.from); + return template.replace('[POKEMON]', this.pokemon(pokemon)); + } + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + if (kwArgs.from && kwArgs.from.startsWith('item:')) { + const template = this.template('endFromItem', status); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(kwArgs.from)); + } + if (kwArgs.thaw) { + const template = this.template('endFromMove', status); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', this.effect(kwArgs.from)); + } + const template = this.template('end', status); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-cureteam': { + return this.template('activate', kwArgs.from); + } + + case '-singleturn': case '-singlemove': { + const [, pokemon, effect] = args; + const line1 = this.maybeAbility(effect, kwArgs.of || pokemon) || this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + let id = this.effectId(effect); + if (id === 'instruct') { + const template = this.template('activate', effect); + return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)).replace('[TARGET]', this.pokemon(pokemon)); + } + const template = this.template('start', effect); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + + case '-sidestart': { + let [, side, effect] = args; + const template = this.template('start', effect); + return template.replace('[TEAM]', this.team(side)); + } + + case '-sideend': { + let [, side, effect] = args; + const template = this.template('end', effect); + return template.replace('[TEAM]', this.team(side)); + } + + case '-weather': { + const [, weather] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of); + if (!weather || weather === 'none') { + return this.template('end', kwArgs.from); + } + if (kwArgs.upkeep) { + return this.template('activate', weather); + } + if (!kwArgs.of) { + return this.template('start', weather); + } + const template = this.template('startFromAbility', weather); + return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)); + } + + case '-fieldstart': case '-fieldactivate': { + const [, effect] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of); + const templateId = cmd.slice(6); + const template = this.template(templateId, effect, 'NODEFAULT'); + return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)); + } + + case '-fieldend': { + let [, effect] = args; + return this.template('end', effect, 'NODEFAULT'); + } + + case '-sethp': { + let effect = kwArgs.from; + return this.template('activate', effect); + } + + case '-message': { + let [, message] = args; + return ' ' + message + '\n'; + } + + case '-hint': { + let [, message] = args; + return ' (' + message + ')\n'; + } + + case '-activate': { + let [, pokemon, effect, target, arg4, arg5] = args; + let id = this.effectId(effect); + if (id === 'celebrate') { + return this.template('activate', 'celebrate').replace('[TRAINER]', this.trainer(pokemon.slice(0, 2))); + } + if (!arg5 && (id === 'spite' || id === 'skillswap')) { + [target, arg4, arg5] = [pokemon, target, arg4]; + } else if (!arg4 && ['grudge', 'forewarn', 'magnitude', 'sketch', 'persistent', 'symbiosis', 'safetygoggles', 'matblock', 'safetygoggles'].includes(id)) { + [target, arg4] = [pokemon, target]; + } else if (!target && ['hyperspacefury', 'hyperspacehole', 'phantomforce', 'shadowforce', 'feint'].includes(id)) { + [pokemon, target] = [kwArgs.of, pokemon]; + if (!pokemon) pokemon = target; + } + if (!target) target = kwArgs.of || pokemon; + + let line1 = this.maybeAbility(effect, pokemon) || this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + if (id === 'wonderguard') { + return line1 + this.template('immune'); + } + + if (['bind', 'wrap', 'clamp', 'whirlpool', 'firespin', 'magmastorm', 'sandtomb', 'infestation', 'charge', 'fairylock', 'trapped'].includes(id)) { + const template = this.template('start', effect); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + if (id === 'lockon' || id === 'mindreader') { + const template = this.template('start', effect); + return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)).replace('[SOURCE]', this.pokemon(pokemon)); + } + if (kwArgs.block) { + return this.template('fail'); + } + if (['ingrain', 'quickguard', 'wideguard', 'craftyshield', 'matblock', 'protect', 'mist', 'safeguard', 'electricterrain', 'mistyterrain', 'psychicterrain', 'telepathy', 'stickyhold', 'suctioncups', 'aromaveil', 'flowerveil', 'sweetveil', 'disguise', 'safetygoggles', 'protectivepads'].includes(id)) { + const template = this.template('block', effect); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', arg4); + } + + let templateId = 'activate'; + if (id === 'forewarn' && pokemon === target) { + templateId = 'activateNoTarget'; + } + let template = this.template(templateId, effect, 'NODEFAULT'); + if (!template) { + template = this.template('activate'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(effect)); + } + + if (id === 'brickbreak') { + template = template.replace('[TEAM]', this.team(target.slice(0, 2))); + } + if (id === 'spite' || id === 'grudge' || id === 'forewarn' || id === 'sketch') { + template = template.replace('[MOVE]', arg4).replace('[NUMBER]', arg5); + } + if (id === 'magnitude') { + template = template.replace('[NUMBER]', arg4); + } + if (id === 'symbiosis') { + template = template.replace('[ITEM]', arg4); + } + if (id === 'skillswap') { + line1 += this.ability(arg4, pokemon); + line1 += this.ability(arg5, target); + } + if (id === 'mummy') { + line1 += this.ability(arg4, target); + line1 += this.ability("Mummy", target); + template = this.template('changeAbility', "Mummy"); + } + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target)).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + + case '-prepare': { + const [, pokemon, effect, target] = args; + const template = this.template('prepare', effect); + return template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target)); + } + + case '-damage': { + let [, pokemon, newHealth, percentage] = args; + let template = this.template('damage', kwArgs.from, 'NODEFAULT'); + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + const id = this.effectId(kwArgs.from); + if (template) { + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + if (!kwArgs.from) { + template = this.template(percentage ? 'damagePercentage' : 'damage'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[PERCENTAGE]', percentage); + } + if (kwArgs.from.startsWith('item:')) { + template = this.template('damageFromItem'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(kwArgs.from)); + } + if (kwArgs.partiallytrapped || id === 'bind' || id === 'wrap') { + template = this.template('damageFromPartialTrapping'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', this.effect(kwArgs.from)); + } + + template = this.template('damage'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-heal': { + let [, pokemon] = args; + let template = this.template('heal', kwArgs.from, 'NODEFAULT'); + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + if (template) { + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of)); + } + + if (kwArgs.from && !kwArgs.from.startsWith('ability:')) { + template = this.template('healFromEffect'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(kwArgs.from)); + } + + template = this.template('heal'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-boost': case '-unboost': { + const [, pokemon, stat, number] = args; + const statName = BattleStats[stat as StatName] || "stats"; + const amount = parseInt(number); + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + let templateId = cmd.slice(1); + if (amount >= 3) templateId += '3'; + else if (amount >= 2) templateId += '2'; + else if (amount === 0) templateId += '0'; + if (amount && kwArgs.zeffect) { + templateId += (kwArgs.multiple ? 'MultipleFromZEffect' : 'FromZEffect'); + } else if (amount && kwArgs.from && kwArgs.from.startsWith('item:')) { + templateId += 'FromItem'; + } + const template = this.template(templateId, kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', statName); + } + + case '-setboost': { + const [, pokemon] = args; + const effect = kwArgs.from; + const line1 = this.maybeAbility(effect, kwArgs.of || pokemon); + const template = this.template('boost', effect); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-swapboost': { + const [, pokemon, target] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + const id = this.effectId(kwArgs.from); + let templateId = 'swapBoost'; + if (id === 'guardswap') templateId = 'swapDefensiveBoost'; + if (id === 'powerswap') templateId = 'swapOffensiveBoost'; + const template = this.template(templateId, kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target)); + } + + case '-copyboost': { + const [, pokemon, target] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + const template = this.template('copyBoost', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target)); + } + + case '-clearboost': case '-clearpositiveboost': case '-clearnegativeboost': { + const [, pokemon, source] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + let templateId = 'clearBoost'; + if (kwArgs.zeffect) templateId = 'clearBoostFromZEffect'; + const template = this.template(templateId, kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(source)); + } + + case '-invertboost': { + const [, pokemon] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + const template = this.template('invertBoost', kwArgs.from); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-clearallboost': { + return this.template('clearAllBoost', kwArgs.from); + } + + case '-crit': case '-supereffective': case '-resisted': { + const [, pokemon] = args; + let templateId = cmd.slice(1); + if (templateId === 'supereffective') templateId = 'superEffective'; + if (kwArgs.spread) templateId += 'Spread'; + const template = this.template(templateId); + return template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-fail': { + let [, pokemon, effect, stat] = args; + let id = this.effectId(effect); + let blocker = this.effectId(kwArgs.from); + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + let templateId = 'block'; + if (['desolateland', 'primordialsea'].includes(blocker) && + !['sunnyday', 'raindance', 'sandstorm', 'hail'].includes(id)) { + templateId = 'blockMove'; + } else if (blocker === 'uproar' && kwArgs.msg) { + templateId = 'blockSelf'; + } + let template = this.template(templateId, kwArgs.from); + if (template) { + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + if (id === 'unboost') { + template = this.template(stat ? 'failSingular' : 'fail', 'unboost'); + if (this.effectId(kwArgs.from) === 'flowerveil') { + template = this.template('block', kwArgs.from); + pokemon = kwArgs.of; + } + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', stat); + } + + templateId = 'fail'; + if (['brn', 'frz', 'par', 'psn', 'slp', 'substitute'].includes(id)) { + templateId = 'alreadyStarted'; + } + if (kwArgs.heavy) templateId = 'failTooHeavy'; + if (kwArgs.weak) templateId = 'fail'; + if (kwArgs.forme) templateId = 'failWrongForme'; + template = this.template(templateId); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-immune': { + let [, pokemon, effect] = args; + const line1 = this.maybeAbility(effect, kwArgs.of || pokemon); + let template = this.template('block', effect); + if (!template) { + const templateId = kwArgs.ohko ? 'immuneOHKO' : 'immune'; + template = this.template(pokemon ? templateId : 'immuneNoPokemon', effect); + } + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-miss': { + const [, source, pokemon] = args; + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); + if (!pokemon) { + const template = this.template('missNoPokemon'); + return line1 + template.replace('[SOURCE]', this.pokemon(source)); + } + const template = this.template('miss'); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-center': case '-ohko': case '-combine': { + return this.template(cmd.slice(1)); + } + + case '-nothing': { + return this.template('activate', 'splash'); + } + + case '-notarget': { + return this.template('noTarget'); + } + + case '-mega': case '-primal': { + const [, pokemon, species, item] = args; + let id = ''; + if (species === 'Rayquaza') id = 'dragonascent'; + if (!id && cmd === '-mega' && this.gen < 7) cmd = '-megaGen6'; + let template = this.template(cmd.slice(1)); + const pokemonName = this.pokemon(pokemon); + if (cmd === '-mega') { + const template2 = this.template('transformMega'); + template += template2.replace('[POKEMON]', pokemonName).replace('[SPECIES]', species); + } + return template.replace('[POKEMON]', pokemonName).replace('[ITEM]', item); + } + + case '-zpower': { + const [, pokemon] = args; + const template = this.template('zPower'); + return template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-burst': { + const [, pokemon] = args; + const template = this.template('activate', "Ultranecrozium Z"); + return template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-zbroken': { + const [, pokemon] = args; + const template = this.template('zBroken'); + return template.replace('[POKEMON]', this.pokemon(pokemon)); + } + + case '-hitcount': { + const [, number] = args; + if (number === '1') { + return this.template('hitCountSingular'); + } + return this.template('hitCount').replace('[NUMBER]', number); + } + + case '-waiting': { + const [, pokemon, target] = args; + const template = this.template('activate', "Water Pledge"); + return template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target)); + } + + case '-anim': { + return ''; + } + + default: { + return null; + } + } + } +} + +exports.BattleTextParser = BattleTextParser; diff --git a/src/battle.ts b/src/battle.ts index fce14f608..4c4e245aa 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -7,6 +7,22 @@ * - The client as a whole is AGPLv3 * - The battle replay/animation engine (battle-*.ts) by itself is MIT * + * Layout: + * + * - Battle + * - Side + * - Pokemon + * - BattleScene + * - BattleLog + * - BattleTextParser + * + * When a Battle receives a message, it splits the message into tokens + * and parses what happens, updating its own state, and then telling + * BattleScene to do any relevant animations. The tokens then get + * passed directly into BattleLog. If the message is an in-battle + * message, it'll be extracted by BattleTextParser, which adds it to + * both the battle log itself, as well as the messagebar. + * * @author Guangcong Luo * @license MIT */ @@ -303,7 +319,7 @@ class Pokemon { this.clearMovestatuses(); this.side.battle.scene.clearEffects(this); } - markMove(moveName: string, pp?: number, recursionSource?: string) { + rememberMove(moveName: string, pp?: number, recursionSource?: string) { if (recursionSource === this.ident) return; if (pp === undefined) pp = 1; moveName = Tools.getMove(moveName).name; @@ -312,7 +328,7 @@ class Pokemon { if (this.volatiles.transform) { // make sure there is no infinite recursion if both Pokemon are transformed into each other if (!recursionSource) recursionSource = this.ident; - this.volatiles.transform[1].markMove(moveName, 0, recursionSource); + this.volatiles.transform[1].rememberMove(moveName, 0, recursionSource); moveName = '*' + moveName; } for (let i = 0; i < this.moveTrack.length; i++) { @@ -324,69 +340,13 @@ class Pokemon { } this.moveTrack.push([moveName, pp]); } - markAbility(ability: string, isNotBase?: boolean) { + rememberAbility(ability: string, isNotBase?: boolean) { ability = Tools.getAbility(ability).name; this.ability = ability; if (!this.baseAbility && !isNotBase) { this.baseAbility = ability; } } - htmlName() { - return '' + BattleLog.escapeHTML(this.name) + ''; - } - getName(shortName?: boolean) { - if (this.side.n === 0) { - return this.htmlName(); - } else { - return (shortName ? "Opposing " : "The opposing ") + this.htmlName(); - } - } - getLowerName(shortName?: boolean) { - if (this.side.n === 0) { - return this.htmlName(); - } else { - return (shortName ? "opposing " : "the opposing ") + this.htmlName(); - } - } - getTitle() { - let titlestring = '(' + this.ability + ') '; - - for (let i = 0; i < this.moves.length; i++) { - if (i != 0) titlestring += ' / '; - titlestring += Tools.getMove(this.moves[i]).name; - } - return titlestring; - } - getFullName(plaintext?: boolean) { - let name = this.side && this.side.n && (this.side.battle.ignoreOpponent || this.side.battle.ignoreNicks) ? this.species : BattleLog.escapeHTML(this.name); - if (name !== this.species) { - if (plaintext) { - name += ' (' + this.species + ')'; - } else { - name = '' + name + ' (' + this.species + ')' + ''; - } - } - if (plaintext) { - if (this === this.side.active[0]) { - name += ' (active)'; - } else if (this.fainted) { - name += ' (fainted)'; - } else { - let statustext = ''; - if (this.hp !== this.maxhp) { - statustext += this.hpDisplay(); - } - if (this.status) { - if (statustext) statustext += '|'; - statustext += this.status; - } - if (statustext) { - name += ' (' + statustext + ')'; - } - } - } - return name; - } getBoost(boostStat: BoostStatName) { let boostStatTable = { atk: 'Atk', @@ -630,14 +590,6 @@ class Side { } if (this.battle.stagnateCallback) this.battle.stagnateCallback(this.battle); } - getTeamName() { - if (this === this.battle.mySide) return "Your team"; - return "The opposing team"; - } - getLowerTeamName() { - if (this === this.battle.mySide) return "your team"; - return "the opposing team"; - } addSideCondition(effect: Effect) { let condition = effect.id; if (this.sideConditions[condition]) { @@ -784,18 +736,11 @@ class Side { pokemon.copyVolatileFrom(this.lastPokemon); } - if (pokemon.side.n === 0) { - this.battle.message('Go! ' + pokemon.getFullName() + '!'); - } else { - this.battle.message('' + BattleLog.escapeHTML(pokemon.side.name) + ' sent out ' + pokemon.getFullName() + '!'); - } - this.battle.scene.animSummon(pokemon, slot); if (this.battle.switchCallback) this.battle.switchCallback(this.battle, this); } dragIn(pokemon: Pokemon, slot = pokemon.slot) { - this.battle.message('' + pokemon.getFullName() + ' was dragged out!'); let oldpokemon = this.active[slot]; if (oldpokemon === pokemon) return; this.lastPokemon = oldpokemon; @@ -849,13 +794,9 @@ class Side { pokemon.removeVolatile('formechange' as ID); } if (pokemon.lastMove === 'uturn' || pokemon.lastMove === 'voltswitch') { - this.battle.message('' + pokemon.getName() + ' went back to ' + BattleLog.escapeHTML(pokemon.side.name) + '!'); + this.battle.log(['switchout', pokemon.ident], {from: pokemon.lastMove}); } else if (pokemon.lastMove !== 'batonpass' && pokemon.lastMove !== 'zbatonpass') { - if (pokemon.side.n === 0) { - this.battle.message('' + pokemon.getName() + ', come back!'); - } else { - this.battle.message('' + BattleLog.escapeHTML(pokemon.side.name) + ' withdrew ' + pokemon.getFullName() + '!'); - } + this.battle.log(['switchout', pokemon.ident]); } pokemon.statusData.toxicTurns = 0; if (this.battle.gen === 5) pokemon.statusData.sleepTurns = 0; @@ -864,22 +805,10 @@ class Side { this.battle.scene.animUnsummon(pokemon); } - swapTo(pokemon: Pokemon, slot: number, kwargs: {[k: string]: string}) { + swapTo(pokemon: Pokemon, slot: number, kwArgs: {[k: string]: string}) { if (pokemon.slot === slot) return; let target = this.active[slot]; - if (!kwargs.silent) { - let fromeffect = Tools.getEffect(kwargs.from); - switch (fromeffect.id) { - case 'allyswitch': - this.battle.message('' + pokemon.getName() + ' and ' + target!.getLowerName() + ' switched places.'); - break; - default: - this.battle.message('' + pokemon.getName() + ' moved to the center!'); - break; - } - } - let oslot = pokemon.slot; pokemon.slot = slot; @@ -894,19 +823,10 @@ class Side { this.battle.scene.animSummon(pokemon, slot, true); if (target) this.battle.scene.animSummon(target, oslot, true); } - swapWith(pokemon: Pokemon, target: Pokemon, kwargs: {[k: string]: string}) { + swapWith(pokemon: Pokemon, target: Pokemon, kwArgs: {[k: string]: string}) { // method provided for backwards compatibility only if (pokemon === target) return; - if (!kwargs.silent) { - let fromeffect = Tools.getEffect(kwargs.from); - switch (fromeffect.id) { - case 'allyswitch': - this.battle.message('' + pokemon.getName() + ' and ' + target.getLowerName() + ' switched places.'); - break; - } - } - let oslot = pokemon.slot; let nslot = target.slot; @@ -926,12 +846,6 @@ class Side { this.lastPokemon = pokemon; this.active[slot] = null; - this.battle.message('' + pokemon.getName() + ' fainted!'); - if (window.Config && Config.server && Config.server.afd && !Config.server.afdFaint) { - this.battle.message('
    Needed that one alive? Buy Max Revive DLC, yours for only $9.99!
    CLICK HERE!
    '); - Config.server.afdFaint = true; - } - pokemon.fainted = true; pokemon.hp = 0; @@ -962,7 +876,7 @@ class Battle { // activity queue activityQueue = [] as string[]; preemptActivityQueue = [] as string[]; - minorQueue = [] as [string[], {[k: string]: string}][]; + waitForAnimations: true | false | 'simult' = true; activityStep = 0; fastForward = 0; fastForwardWillScroll = false; @@ -985,7 +899,6 @@ class Battle { messageFadeTime = 300; messageShownTime = 1; turnsSinceMoved = 0; - hasPreMoveMessage = false; turn = 0; /** @@ -1095,7 +1008,6 @@ class Battle { this.activeMoveIsSpread = null; this.activityStep = 0; this.fastForwardOff(); - this.minorQueue = []; this.resultWaiting = false; this.paused = true; if (this.playbackState !== Playback.Seeking) { @@ -1117,21 +1029,21 @@ class Battle { this.p2 = null!; } - message(message: string, hiddenMessage?: string) { - this.scene.message(message, hiddenMessage); + log(args: Args, kwArgs?: KWArgs, preempt?: boolean) { + this.scene.log.add(args, kwArgs, preempt); } switchSides(replay?: boolean) { if (this.ended) { - this.reset(true); this.setSidesSwitched(!this.sidesSwitched); + this.reset(true); this.fastForwardTo(-1); } else { let turn = this.turn; let paused = this.paused; + this.setSidesSwitched(!this.sidesSwitched); this.reset(true); this.paused = paused; - this.setSidesSwitched(!this.sidesSwitched); if (turn) this.fastForwardTo(turn); if (!paused) { this.play(); @@ -1153,8 +1065,6 @@ class Battle { this.sides[1] = this.yourSide; this.sides[0].n = 0; this.sides[1].n = 1; - this.sides[0].updateSprites(); - this.sides[1].updateSprites(); // nothing else should need updating - don't call this function after sending out pokemon } @@ -1163,16 +1073,15 @@ class Battle { // activities // start() { - this.scene.log('
    Battle between ' + BattleLog.escapeHTML(this.p1.name) + ' and ' + BattleLog.escapeHTML(this.p2.name) + ' started!
    '); + this.log(['start']); if (this.startCallback) this.startCallback(this); } winner(winner?: string) { - if (winner) this.message('' + BattleLog.escapeHTML(winner) + ' won the battle!'); - else this.message('Tie between ' + BattleLog.escapeHTML(this.p1.name) + ' and ' + BattleLog.escapeHTML(this.p2.name) + '!'); + this.log(['win', winner || '']); this.ended = true; } prematureEnd() { - this.message('This replay ends here.'); + this.log(['message', 'This replay ends here.']); this.ended = true; } endLastTurn() { @@ -1204,7 +1113,6 @@ class Battle { if (!this.fastForward) this.turnsSinceMoved++; this.scene.incrementTurn(); - this.scene.log('

    Turn ' + turnNum + '

    '); if (this.fastForward) { if (this.turnCallback) this.turnCallback(this); @@ -1231,55 +1139,9 @@ class Battle { } changeWeather(weatherName: string, poke?: Pokemon, isUpkeep?: boolean, ability?: Effect) { let weather = toId(weatherName); - let weatherTable = { - sunnyday: { - name: 'Sun', - startMessage: 'The sunlight turned harsh!', - abilityMessage: "'s Drought intensified the sun's rays!", - //upkeepMessage: 'The sunlight is strong!', - endMessage: "The sunlight faded." - }, - desolateland: { - name: "Intense Sun", - startMessage: "The sunlight turned extremely harsh!", - endMessage: "The harsh sunlight faded." - }, - raindance: { - name: 'Rain', - startMessage: 'It started to rain!', - abilityMessage: "'s Drizzle made it rain!", - //upkeepMessage: 'Rain continues to fall!', - endMessage: 'The rain stopped.' - }, - primordialsea: { - name: "Heavy Rain", - startMessage: "A heavy rain began to fall!", - endMessage: "The heavy rain has lifted!" - }, - sandstorm: { - name: 'Sandstorm', - startMessage: 'A sandstorm kicked up!', - abilityMessage: "'s Sand Stream whipped up a sandstorm!", - upkeepMessage: 'The sandstorm is raging.', - endMessage: 'The sandstorm subsided.' - }, - hail: { - name: 'Hail', - startMessage: 'It started to hail!', - abilityMessage: "'s Snow Warning whipped up a hailstorm!", - upkeepMessage: 'The hail is crashing down.', - endMessage: 'The hail stopped.' - }, - deltastream: { - name: 'Strong Winds', - startMessage: 'Mysterious strong winds are protecting Flying-type Pokémon!', - endMessage: 'The mysterious strong winds have dissipated!' - } - } as {[weatherid: string]: {name: string, startMessage: string, abilityMessage?: string, upkeepMessage?: string, endMessage: string}}; if (!weather || weather === 'none') { weather = '' as ID; } - let newWeather = weatherTable[weather]; if (isUpkeep) { if (this.weather && this.weatherTimeLeft) { this.weatherTimeLeft--; @@ -1288,37 +1150,24 @@ class Battle { if (!this.fastForward) { this.scene.upkeepWeather(); } - if (newWeather && newWeather.upkeepMessage) { - this.message('
    ' + newWeather.upkeepMessage + '
    '); - } return; } - if (newWeather) { + if (weather) { let isExtremeWeather = (weather === 'deltastream' || weather === 'desolateland' || weather === 'primordialsea'); if (poke) { if (ability) { - this.scene.abilityActivateAnim(poke, ability.name); - this.message('', "[" + poke.getName(true) + "'s " + ability.name + "!]"); - poke.markAbility(ability.name); - this.message('' + newWeather.startMessage + ''); - } else { - this.message('' + poke.getName() + newWeather.abilityMessage + ''); // for backwards compatibility + this.activateAbility(poke, ability.name); } this.weatherTimeLeft = (this.gen <= 5 || isExtremeWeather) ? 0 : 8; this.weatherMinTimeLeft = (this.gen <= 5 || isExtremeWeather) ? 0 : 5; } else if (isExtremeWeather) { - this.message('' + newWeather.startMessage + ''); this.weatherTimeLeft = 0; this.weatherMinTimeLeft = 0; } else { - this.message('' + newWeather.startMessage + ''); this.weatherTimeLeft = (this.gen <= 3 ? 5 : 8); this.weatherMinTimeLeft = (this.gen <= 3 ? 0 : 5); } } - if (this.weather && !newWeather) { - this.message('' + weatherTable[this.weather].endMessage + ''); - } this.weather = weather; this.scene.updateWeather(); } @@ -1337,8 +1186,8 @@ class Battle { } this.scene.updateWeather(); } - useMove(pokemon: Pokemon, move: Move, target: Pokemon | null, kwargs: {[k: string]: string}) { - let fromeffect = Tools.getEffect(kwargs.from); + useMove(pokemon: Pokemon, move: Move, target: Pokemon | null, kwArgs: {[k: string]: string}) { + let fromeffect = Tools.getEffect(kwArgs.from); pokemon.clearMovestatuses(); if (move.id === 'focuspunch') { pokemon.removeTurnstatus('focuspunch' as ID); @@ -1350,149 +1199,42 @@ class Battle { if (!target) { target = pokemon.side.foe.missedPokemon; } - if (!kwargs.silent) { - if (kwargs.zeffect) { - this.message('' + pokemon.getName() + ' unleashes its full-force Z-Move!', ''); - } - switch (fromeffect.id) { - case 'snatch': - break; - case 'magicbounce': - case 'magiccoat': - case 'rebound': - if (fromeffect.id === 'magiccoat') { - this.scene.resultAnim(pokemon, "Bounced", 'good'); - pokemon.addTurnstatus('magiccoat' as ID); - } else { - this.scene.abilityActivateAnim(pokemon, fromeffect.name); - this.message('', "[" + pokemon.getName(true) + "'s " + fromeffect.name + "!]"); - pokemon.markAbility(fromeffect.name); - } - this.message(pokemon.getName() + " bounced the " + move.name + " back!"); - break; - case 'metronome': - this.message('Waggling a finger let it use ' + move.name + '!'); - break; - case 'naturepower': - this.message('Nature Power turned into ' + move.name + '!'); - break; - case 'weatherball': - this.message('Breakneck Blitz turned into ' + move.name + ' due to the weather!'); - break; - case 'sleeptalk': - pokemon.markMove(move.name, 0); - this.message(pokemon.getName() + ' used ' + move.name + '!'); - break; - // Gen 1 - case 'bind': - case 'clamp': - case 'firespin': - case 'wrap': - this.message(pokemon.getName() + "'s attack continues!"); - break; - default: - // April Fool's 2014 - if (window.Config && Config.server && Config.server.afd && move.id === 'earthquake') { - if (!this.fastForward && window.$) { - $('body').css({ - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0 - }).animate({ - left: -30, - right: 30 - }, 75).animate({ - left: 30, - right: -30 - }, 100).animate({ - left: -30, - right: 30 - }, 100).animate({ - left: 30, - right: -30 - }, 100).animate({ - left: 0, - right: 0 - }, 100, function () { - $(this).css({ - position: 'static' - }); - }); + if (fromeffect.id === 'sleeptalk') { + pokemon.rememberMove(move.name, 0); + } else if (!fromeffect.id || fromeffect.id === 'pursuit') { + let moveName = move.name; + if (move.isZ) { + pokemon.item = move.isZ; + let item = Tools.getItem(move.isZ); + if (item.zMoveFrom) moveName = item.zMoveFrom; + } else if (move.name.slice(0, 2) === 'Z-') { + moveName = moveName.slice(2); + move = Tools.getMove(moveName); + if (window.BattleItems) { + for (let item in BattleItems) { + if (BattleItems[item].zMoveType === move.type) pokemon.item = item; } - this.message(pokemon.getName() + ' used Fissure!'); - this.message('Just kidding! It was Earthquake!'); - } else if (window.Config && Config.server && Config.server.afd && move.id === 'stealthrock') { - let srNames = ['Sneaky Pebbles', 'Sly Rubble', 'Subtle Sediment', 'Buried Bedrock', 'Camouflaged Cinnabar', 'Clandestine Cobblestones', 'Cloaked Clay', 'Concealed Ore', 'Covert Crags', 'Crafty Coal', 'Discreet Bricks', 'Disguised Debris', 'Espionage Pebbles', 'Furtive Fortress', 'Hush-Hush Hardware', 'Incognito Boulders', 'Invisible Quartz', 'Masked Minerals', 'Mischievous Masonry', 'Obscure Ornaments', 'Private Paragon', 'Secret Solitaire', 'Sheltered Sand', 'Surreptitious Sapphire', 'Undercover Ultramarine']; - this.message(pokemon.getName() + ' used ' + srNames[Math.floor(Math.random() * srNames.length)] + '!'); - } else if (window.Config && Config.server && Config.server.afd && move.id === 'extremespeed') { - let fastWords = ['H-Hayai', 'Masaka', 'Its fast']; - this.message(pokemon.getName() + ' used ' + move.name + '!'); - this.message('' + fastWords[Math.floor(Math.random() * fastWords.length)] + '!'); - } else if (window.Config && Config.server && Config.server.afd && move.id === 'aerialace') { - this.message(pokemon.getName() + ' used Tsubame Gaeshi!'); - // } else if (window.Config && Config.server && Config.server.afd && (move.id === 'metronome' || move.id === 'sleeptalk' || move.id === 'assist')) { - // this.message(pokemon.getName() + ' used ' + move.name + '!'); - // let buttons = ["A", "B", "START", "SELECT", "UP", "DOWN", "LEFT", "RIGHT", "DEMOCRACY", "ANARCHY"]; - // let people = ["Zarel", "The Immortal", "Diatom", "Nani Man", "shaymin", "apt-get", "sirDonovan", "Arcticblast", "Trickster"]; - // let button; - // for (let i = 0; i < 10; i++) { - // let name = people[Math.floor(Math.random() * people.length)]; - // if (!button) button = buttons[Math.floor(Math.random() * buttons.length)]; - // this.scene.log('
    ' + BattleLog.escapeHTML(name) + ': ' + button + '
    '); - // button = (name === 'Diatom' ? "thanks diatom" : null); - // } - } else { - this.message(pokemon.getName() + ' used ' + move.name + '!'); } - if (!fromeffect.id || fromeffect.id === 'pursuit') { - let moveName = move.name; - if (move.isZ) { - pokemon.item = move.isZ; - let item = Tools.getItem(move.isZ); - if (item.zMoveFrom) moveName = item.zMoveFrom; - } else if (move.name.slice(0, 2) === 'Z-') { - moveName = moveName.slice(2); - move = Tools.getMove(moveName); - if (window.BattleItems) { - for (let item in BattleItems) { - if (BattleItems[item].zMoveType === move.type) pokemon.item = item; - } - } - } - let pp = (target && target.side !== pokemon.side && toId(target.ability) === 'pressure' ? 2 : 1); - pokemon.markMove(moveName, pp); - } - break; - } - if (window.Config && Config.server && Config.server.afd && move.id === 'taunt') { - let quotes = [ - "Yo mama so fat, she 4x resists Ice- and Fire-type attacks!", - "Yo mama so ugly, Captivate raises her opponent's Special Attack!", - "Yo mama so dumb, she lowers her Special Attack when she uses Nasty Plot!", - "Yo mama so dumb, she thought Sylveon would be Light Type!" - ]; - let quote = quotes[(this.p1.name.charCodeAt(2) + this.p2.name.charCodeAt(2) + this.turn) % quotes.length]; - this.message(pokemon.getName() + " said, \"" + quote + "\""); } + let pp = (target && target.side !== pokemon.side && toId(target.ability) === 'pressure' ? 2 : 1); + pokemon.rememberMove(moveName, pp); } - if (!this.fastForward && !kwargs.still) { + if (!this.fastForward && !kwArgs.still) { // skip - if (kwargs.miss && target.side) { + if (kwArgs.miss && target.side) { target = target.side.missedPokemon; } - if (kwargs.notarget || !target) { + if (kwArgs.notarget || !target) { target = pokemon.side.foe.missedPokemon; } - if (kwargs.prepare || kwargs.anim === 'prepare') { + if (kwArgs.prepare || kwArgs.anim === 'prepare') { this.scene.runPrepareAnim(move.id, pokemon, target); - } else if (!kwargs.notarget) { - let usedMove = kwargs.anim ? Tools.getMove(kwargs.anim) : move; - if (kwargs.spread) { - this.activeMoveIsSpread = kwargs.spread; + } else if (!kwArgs.notarget) { + let usedMove = kwArgs.anim ? Tools.getMove(kwArgs.anim) : move; + if (kwArgs.spread) { + this.activeMoveIsSpread = kwArgs.spread; let targets = [pokemon]; - let hitPokemon = kwargs.spread.split(','); + let hitPokemon = kwArgs.spread.split(','); if (hitPokemon[0] !== '.') { for (const target of hitPokemon) { targets.push(this.getPokemon(target + ': ?')!); @@ -1514,2557 +1256,1370 @@ class Battle { pokemon.side.wisher = pokemon; } } - cantUseMove(pokemon: Pokemon, effect: Effect, move: Move, kwargs: {[k: string]: string}) { + cantUseMove(pokemon: Pokemon, effect: Effect, move: Move, kwArgs: {[k: string]: string}) { pokemon.clearMovestatuses(); this.scene.updateStatbar(pokemon); if (effect.id in BattleStatusAnims) { this.scene.runStatusAnim(effect.id, [pokemon]); } - if (effect.effectType === 'Ability') { - this.scene.abilityActivateAnim(pokemon, effect.name); - this.message('', "[" + pokemon.getName(true) + "'s " + effect.name + "!]"); - pokemon.markAbility(effect.name); - } + this.activateAbility(pokemon, effect); + if (move.id) pokemon.rememberMove(move.name, 0); switch (effect.id) { - case 'taunt': - this.message('' + pokemon.getName() + ' can\'t use ' + move.name + ' after the taunt!'); - pokemon.markMove(move.name, 0); - break; - case 'gravity': - this.message('' + pokemon.getName() + ' can\'t use ' + move.name + ' because of gravity!'); - pokemon.markMove(move.name, 0); - break; - case 'healblock': - this.message('' + pokemon.getName() + ' can\'t use ' + move.name + ' because of Heal Block!'); - pokemon.markMove(move.name, 0); - break; - case 'imprison': - this.message('' + pokemon.getName() + ' can\'t use its sealed ' + move.name + '!'); - pokemon.markMove(move.name, 0); - break; - case 'throatchop': - this.message('The effects of Throat Chop prevent ' + pokemon.getName() + ' from using certain moves!'); - break; case 'par': this.scene.resultAnim(pokemon, 'Paralyzed', 'par'); - this.message('' + pokemon.getName() + ' is paralyzed! It can\'t move!'); break; case 'frz': this.scene.resultAnim(pokemon, 'Frozen', 'frz'); - this.message('' + pokemon.getName() + ' is frozen solid!'); break; case 'slp': this.scene.resultAnim(pokemon, 'Asleep', 'slp'); pokemon.statusData.sleepTurns++; - this.message('' + pokemon.getName() + ' is fast asleep.'); - break; - case 'skydrop': - this.message('Sky Drop won\'t let ' + pokemon.getLowerName() + ' go!'); - break; - case 'damp': - case 'dazzling': - case 'queenlymajesty': - let ofpoke = this.getPokemon(kwargs.of)!; - this.message(ofpoke.getName() + ' cannot use ' + move.name + '!'); break; case 'truant': this.scene.resultAnim(pokemon, 'Loafing around', 'neutral'); - this.message('' + pokemon.getName() + ' is loafing around!'); break; case 'recharge': this.scene.runOtherAnim('selfstatus' as ID, [pokemon]); this.scene.resultAnim(pokemon, 'Must recharge', 'neutral'); - this.message('' + pokemon.getName() + ' must recharge!'); break; case 'focuspunch': this.scene.resultAnim(pokemon, 'Lost focus', 'neutral'); - this.message(pokemon.getName() + ' lost its focus and couldn\'t move!'); pokemon.removeTurnstatus('focuspunch' as ID); break; case 'shelltrap': this.scene.resultAnim(pokemon, 'Trap failed', 'neutral'); - this.message(pokemon.getName() + '\'s shell trap didn\'t work!'); pokemon.removeTurnstatus('shelltrap' as ID); break; case 'flinch': this.scene.resultAnim(pokemon, 'Flinched', 'neutral'); - this.message(pokemon.getName() + ' flinched and couldn\'t move!'); pokemon.removeTurnstatus('focuspunch' as ID); break; case 'attract': this.scene.resultAnim(pokemon, 'Immobilized', 'neutral'); - this.message(pokemon.getName() + ' is immobilized by love!'); - break; - case 'nopp': - this.message(pokemon.getName() + ' used ' + move.name + '!'); - this.message('But there was no PP left for the move!'); - break; - default: - this.message('' + pokemon.getName() + (move.name ? ' can\'t use ' + move.name + '' : ' can\'t move') + '!'); break; } this.scene.animReset(pokemon); } - runMinor(args?: string[], kwargs?: {[k: string]: string}, preempt?: boolean, nextArgs?: string[], nextKwargs?: {[k: string]: string}) { - let actions = ''; - let minors = this.minorQueue; - if (args && kwargs && nextArgs && nextKwargs) { + + activateAbility(pokemon: Pokemon | null, effectOrName: Effect | string, isNotBase?: boolean) { + if (!pokemon || !effectOrName) return; + if (typeof effectOrName !== 'string') { + if (effectOrName.effectType !== 'Ability') return; + effectOrName = effectOrName.name; + } + this.scene.abilityActivateAnim(pokemon, effectOrName); + pokemon.rememberAbility(effectOrName, isNotBase); + } + + runMinor(args: Args, kwArgs: KWArgs, nextArgs?: Args, nextKwargs?: KWArgs) { + if (nextArgs && nextKwargs) { if (args[2] === 'Sturdy' && args[0] === '-activate') args[2] = 'ability: Sturdy'; - if (args[0] === '-crit' || args[0] === '-supereffective' || args[0] === '-resisted' || args[2] === 'ability: Sturdy') kwargs.then = '.'; - if (args[0] === '-damage' && !kwargs.from && args[1] !== nextArgs[1] && (nextArgs[0] === '-crit' || nextArgs[0] === '-supereffective' || nextArgs[0] === '-resisted' || (nextArgs[0] === '-damage' && !nextKwargs.from))) kwargs.then = '.'; - if (args[0] === '-damage' && nextArgs[0] === '-damage' && kwargs.from && kwargs.from === nextKwargs.from) kwargs.then = '.'; - if (args[0] === '-ability' && (args[2] === 'Intimidate' || args[3] === 'boost')) kwargs.then = '.'; - if (args[0] === '-unboost' && nextArgs[0] === '-unboost') kwargs.then = '.'; - if (args[0] === '-boost' && nextArgs[0] === '-boost') kwargs.then = '.'; - if (args[0] === '-damage' && kwargs.from === 'Leech Seed' && nextArgs[0] === '-heal' && nextKwargs.silent) kwargs.then = '.'; - minors.push([args, kwargs]); - if (kwargs.simult || kwargs.then) { - return; + if (args[0] === '-crit' || args[0] === '-supereffective' || args[0] === '-resisted' || args[2] === 'ability: Sturdy') kwArgs.then = '.'; + if (args[0] === '-damage' && !kwArgs.from && args[1] !== nextArgs[1] && (nextArgs[0] === '-crit' || nextArgs[0] === '-supereffective' || nextArgs[0] === '-resisted' || (nextArgs[0] === '-damage' && !nextKwargs.from))) kwArgs.then = '.'; + if (args[0] === '-damage' && nextArgs[0] === '-damage' && kwArgs.from && kwArgs.from === nextKwargs.from) kwArgs.then = '.'; + if (args[0] === '-ability' && (args[2] === 'Intimidate' || args[3] === 'boost')) kwArgs.then = '.'; + if (args[0] === '-unboost' && nextArgs[0] === '-unboost') kwArgs.then = '.'; + if (args[0] === '-boost' && nextArgs[0] === '-boost') kwArgs.then = '.'; + if (args[0] === '-damage' && kwArgs.from === 'Leech Seed' && nextArgs[0] === '-heal' && nextKwargs.silent) kwArgs.then = '.'; + if (args[0] === 'detailschange' && nextArgs[0] === '-mega') { + if (this.scene.closeMessagebar()) { + this.activityStep--; + return; + } + kwArgs.simult = '.'; } } - while (minors.length) { - let row = minors.shift()!; - args = row[0]; - kwargs = row[1]; - if (kwargs.simult) this.scene.timeOffset = 0; + if (kwArgs.then) this.waitForAnimations = false; + if (kwArgs.simult) this.waitForAnimations = 'simult'; - switch (args[0]) { - case '-center': { - actions += "Automatic center!"; - break; - } case '-damage': { - let poke = this.getPokemon(args[1])!; - let damage = poke.healthParse(args[2], true); - if (damage === null) break; - let range = poke.getDamageRange(damage); + switch (args[0]) { + case '-damage': { + let poke = this.getPokemon(args[1])!; + let damage = poke.healthParse(args[2], true); + if (damage === null) break; + let range = poke.getDamageRange(damage); - if (kwargs.silent) { - // do nothing - } else if (kwargs.from) { - let effect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of); - if (effect.effectType === 'Ability' && ofpoke) { - this.scene.abilityActivateAnim(ofpoke, effect.name); - this.message('', "[" + ofpoke.getName(true) + "'s " + effect.name + "!]"); - ofpoke.markAbility(effect.name); - } else if (effect.effectType === 'Item') { - (ofpoke || poke).item = effect.name; - } - switch (effect.id) { - case 'stealthrock': - actions += "Pointed stones dug into " + poke.getLowerName() + "! "; - break; - case 'spikes': - actions += "" + poke.getName() + " is hurt by the spikes! "; - break; - case 'brn': - this.scene.runStatusAnim('brn' as ID, [poke]); - actions += "" + poke.getName() + " was hurt by its burn! "; - break; - case 'psn': - this.scene.runStatusAnim('psn' as ID, [poke]); - actions += "" + poke.getName() + " was hurt by poison! "; - break; - case 'lifeorb': - this.message('', '' + poke.getName() + ' lost some of its HP!'); - break; - case 'recoil': - actions += "" + poke.getName() + " is damaged by the recoil! "; - break; - case 'sandstorm': - actions += "" + poke.getName() + " is buffeted by the sandstorm! "; - break; - case 'hail': - actions += "" + poke.getName() + " is buffeted by the hail! "; - break; - case 'baddreams': - this.scene.runStatusAnim('cursed' as ID, [poke]); - actions += "" + poke.getName() + " is tormented!"; - break; - case 'curse': - this.scene.runStatusAnim('cursed' as ID, [poke]); - actions += "" + poke.getName() + " is afflicted by the curse! "; - break; - case 'nightmare': - actions += "" + poke.getName() + " is locked in a nightmare! "; - break; - case 'roughskin': - case 'ironbarbs': - case 'spikyshield': - actions += "" + poke.getName() + " was hurt! "; - break; - case 'innardsout': - case 'aftermath': - actions += "" + poke.getName() + " is hurt! "; - break; - case 'liquidooze': - actions += "" + poke.getName() + " sucked up the liquid ooze! "; - break; - case 'dryskin': - case 'solarpower': - break; - case 'confusion': - this.scene.runStatusAnim('confusedselfhit' as ID, [poke]); - actions += "It hurt itself in its confusion! "; - this.hasPreMoveMessage = false; - break; - case 'leechseed': - this.scene.runOtherAnim('leech' as ID, [ofpoke!, poke]); - actions += "" + poke.getName() + "'s health is sapped by Leech Seed! "; - break; - case 'flameburst': - actions += "The bursting flame hit " + poke.getLowerName() + "! "; - break; - case 'firepledge': - actions += "" + poke.getName() + " is hurt by the sea of fire! "; - break; - case 'jumpkick': - case 'highjumpkick': - actions += "" + poke.getName() + " kept going and crashed!"; - break; - case 'bind': - case 'wrap': - this.scene.runOtherAnim('bound' as ID, [poke]); - actions += "" + poke.getName() + ' is hurt by ' + effect.name + '!'; - break; - default: - if (ofpoke) { - actions += "" + poke.getName() + " is hurt by " + ofpoke.getLowerName() + "'s " + effect.name + "! "; - } else if (effect.effectType === 'Item') { - actions += "" + poke.getName() + " is hurt by its " + effect.name + "! "; - } else if (effect.effectType === 'Ability') { - actions += "" + poke.getName() + " is hurt by its " + effect.name + "! "; - } else if (kwargs.partiallytrapped) { - actions += "" + poke.getName() + ' is hurt by ' + effect.name + '! '; - } else { - actions += "" + poke.getName() + " lost some HP because of " + effect.name + "! "; - } - break; - } - } else { - let damageinfo = '' + poke.getFormattedRange(range, damage[1] === 100 ? 0 : 1, '–'); - if (damage[1] !== 100) { - let hover = '' + ((damage[0] < 0) ? '−' : '') + - Math.abs(damage[0]) + '/' + damage[1]; - if (damage[1] === 48) { // this is a hack - hover += ' pixels'; - } - damageinfo = '' + damageinfo + ''; - } - let hiddenactions = '' + poke.getName() + ' lost ' + damageinfo + ' of its health!
    '; - this.message(actions ? '' + actions + '' : '', hiddenactions); - actions = ''; - } - this.scene.damageAnim(poke, poke.getFormattedRange(range, 0, ' to ')); - break; - } case '-heal': { - let poke = this.getPokemon(args[1])!; - let damage = poke.healthParse(args[2], true, true); - if (damage === null) break; - let range = poke.getDamageRange(damage); - - if (kwargs.silent) { - // do nothing - } else if (kwargs.from) { - let effect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of); - if (effect.effectType === 'Ability') { - this.scene.abilityActivateAnim(poke, effect.name); - this.message('', "[" + poke.getName(true) + "'s " + effect.name + "!]"); - poke.markAbility(effect.name); - } - switch (effect.id) { - case 'memento': - case 'partingshot': - actions += "" + poke.getName() + "'s HP was restored by the Z-Power!"; - break; - case 'ingrain': - actions += "" + poke.getName() + " absorbed nutrients with its roots!"; - break; - case 'aquaring': - actions += "A veil of water restored " + poke.getLowerName() + "'s HP!"; - break; - case 'healingwish': - actions += "The healing wish came true for " + poke.getLowerName() + "!"; - this.lastMove = 'healing-wish'; - this.scene.runResidualAnim('healingwish' as ID, poke); - poke.side.wisher = null; - break; - case 'lunardance': - actions += "" + poke.getName() + " became cloaked in mystical moonlight!"; - this.lastMove = 'healing-wish'; - this.scene.runResidualAnim('healingwish' as ID, poke); - for (let trackedMove of poke.moveTrack) { - trackedMove[1] = 0; - } - poke.side.wisher = null; - break; - case 'wish': - actions += "" + kwargs.wisher + "'s wish came true!"; - this.scene.runResidualAnim('wish' as ID, poke); - break; - case 'drain': - actions += ofpoke!.getName() + ' had its energy drained!'; - break; - case 'leftovers': - case 'shellbell': - case 'blacksludge': - poke.item = effect.name; - actions += "" + poke.getName() + " restored a little HP using its " + effect.name + "!"; - break; - default: - if (kwargs.absorb) { - actions += "" + poke.getName() + "'s " + effect.name + " absorbs the attack!"; - } else if (effect.id && effect.effectType !== 'Ability') { - actions += "" + poke.getName() + " restored HP using its " + effect.name + "!"; - } else { - actions += poke.getName() + ' restored its HP.'; - } - break; - } - } else if (kwargs.zeffect) { - actions += "" + poke.getName() + " restored its HP using its Z-Power!"; - } else { - actions += poke.getName() + ' restored its HP.'; - } - this.scene.runOtherAnim('heal' as ID, [poke]); - this.scene.healAnim(poke, poke.getFormattedRange(range, 0, ' to ')); - break; - } case '-sethp': { - let effect = Tools.getEffect(kwargs.from); - // let poke, ofpoke; - for (let k = 0; k < 2; k++) { - let cpoke = this.getPokemon(args[1 + 2 * k]); - if (cpoke) { - let damage = cpoke.healthParse(args[2 + 2 * k])!; - let range = cpoke.getDamageRange(damage); - let formattedRange = cpoke.getFormattedRange(range, 0, ' to '); - let diff = damage[0]; - if (diff > 0) { - this.scene.healAnim(cpoke, formattedRange); - } else { - this.scene.damageAnim(cpoke, formattedRange); - } - } - // if (k == 0) poke = cpoke; - // if (k == 1) ofpoke = cpoke; + if (kwArgs.from) { + let effect = Tools.getEffect(kwArgs.from); + let ofpoke = this.getPokemon(kwArgs.of); + this.activateAbility(ofpoke, effect); + if (effect.effectType === 'Item') { + (ofpoke || poke).item = effect.name; } switch (effect.id) { - case 'painsplit': - actions += 'The battlers shared their pain!'; + case 'brn': + this.scene.runStatusAnim('brn' as ID, [poke]); + break; + case 'psn': + this.scene.runStatusAnim('psn' as ID, [poke]); + break; + case 'baddreams': + this.scene.runStatusAnim('cursed' as ID, [poke]); + break; + case 'curse': + this.scene.runStatusAnim('cursed' as ID, [poke]); + break; + case 'confusion': + this.scene.runStatusAnim('confusedselfhit' as ID, [poke]); + break; + case 'leechseed': + this.scene.runOtherAnim('leech' as ID, [ofpoke!, poke]); + break; + case 'bind': + case 'wrap': + this.scene.runOtherAnim('bound' as ID, [poke]); break; } + } else { + let damageinfo = '' + poke.getFormattedRange(range, damage[1] === 100 ? 0 : 1, '\u2013'); + if (damage[1] !== 100) { + let hover = '' + ((damage[0] < 0) ? '\u2212' : '') + + Math.abs(damage[0]) + '/' + damage[1]; + if (damage[1] === 48) { // this is a hack + hover += ' pixels'; + } + // battle-log will convert this into + damageinfo = '||' + hover + '||' + damageinfo + '||'; + } + args[3] = damageinfo; + } + this.scene.damageAnim(poke, poke.getFormattedRange(range, 0, ' to ')); + this.log(args, kwArgs); + break; + } + case '-heal': { + let poke = this.getPokemon(args[1])!; + let damage = poke.healthParse(args[2], true, true); + if (damage === null) break; + let range = poke.getDamageRange(damage); - break; - - } case '-boost': { - let poke = this.getPokemon(args[1])!; - let stat = args[2] as BoostStatName; - if (this.gen === 1 && stat === 'spd') break; - if (this.gen === 1 && stat === 'spa') stat = 'spc'; - let amount = parseInt(args[3], 10); - if (amount === 0) { - actions += "" + poke.getName() + "'s " + BattleStats[stat] + " won't go any higher! "; - this.scene.resultAnim(poke, 'Highest ' + BattleStats[stat], 'good'); + if (kwArgs.from) { + let effect = Tools.getEffect(kwArgs.from); + let ofpoke = this.getPokemon(kwArgs.of); + this.activateAbility(ofpoke || poke, effect); + if (effect.effectType === 'Item') { + poke.item = effect.name; + } + switch (effect.id) { + case 'lunardance': + for (let trackedMove of poke.moveTrack) { + trackedMove[1] = 0; + } + // falls through + case 'healingwish': + this.lastMove = 'healing-wish'; + this.scene.runResidualAnim('healingwish' as ID, poke); + poke.side.wisher = null; + break; + case 'wish': + this.scene.runResidualAnim('wish' as ID, poke); break; } - if (!poke.boosts[stat]) { - poke.boosts[stat] = 0; - } - poke.boosts[stat] += amount; - - let amountString = ''; - if (amount === 2) amountString = ' sharply'; - if (amount >= 3) amountString = ' drastically'; - if (kwargs.silent) { - // do nothing - } else if (kwargs.from) { - let effect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of); - if (effect.effectType === 'Ability' && !(effect.id === 'weakarmor' && stat === 'spe')) { - this.scene.abilityActivateAnim(ofpoke || poke, effect.name); - this.message('', "[" + (ofpoke || poke).getName(true) + "'s " + effect.name + "!]"); - poke.markAbility(effect.name); - } - switch (effect.id) { - default: - if (effect.effectType === 'Ability') { - actions += "" + poke.getName() + "'s " + BattleStats[stat] + " rose" + amountString + "! "; - } - if (effect.effectType === 'Item') { - actions += "The " + effect.name + amountString + " raised " + poke.getLowerName() + "'s " + BattleStats[stat] + "! "; - } - break; - } - } else if (kwargs.zeffect) { - if (minors.length && minors[0][1].zeffect) { - actions += "" + poke.getName() + " boosted its stats" + amountString + " using its Z-Power! "; - for (let i = 0; i < minors.length; i++) { - minors[i][1].silent = '.'; - } + } + this.scene.runOtherAnim('heal' as ID, [poke]); + this.scene.healAnim(poke, poke.getFormattedRange(range, 0, ' to ')); + this.log(args, kwArgs); + break; + } + case '-sethp': { + for (let k = 0; k < 2; k++) { + let cpoke = this.getPokemon(args[1 + 2 * k]); + if (cpoke) { + let damage = cpoke.healthParse(args[2 + 2 * k])!; + let range = cpoke.getDamageRange(damage); + let formattedRange = cpoke.getFormattedRange(range, 0, ' to '); + let diff = damage[0]; + if (diff > 0) { + this.scene.healAnim(cpoke, formattedRange); } else { - actions += "" + poke.getName() + " boosted its " + BattleStats[stat] + amountString + " using its Z-Power! "; + this.scene.damageAnim(cpoke, formattedRange); } - } else { - actions += "" + poke.getName() + "'s " + BattleStats[stat] + " rose" + amountString + "! "; } - this.scene.resultAnim(poke, poke.getBoost(stat), 'good'); + } + this.log(args, kwArgs); + break; + } + case '-boost': { + let poke = this.getPokemon(args[1])!; + let stat = args[2] as BoostStatName; + if (this.gen === 1 && stat === 'spd') break; + if (this.gen === 1 && stat === 'spa') stat = 'spc'; + let amount = parseInt(args[3], 10); + if (amount === 0) { + this.scene.resultAnim(poke, 'Highest ' + BattleStats[stat], 'neutral'); + this.log(args, kwArgs); break; - } case '-unboost': { - let poke = this.getPokemon(args[1])!; - let stat = args[2] as BoostStatName; - if (this.gen === 1 && stat === 'spd') break; - if (this.gen === 1 && stat === 'spa') stat = 'spc'; - let amount = parseInt(args[3], 10); - if (amount === 0) { - actions += "" + poke.getName() + "'s " + BattleStats[stat] + " won't go any lower! "; - this.scene.resultAnim(poke, 'Lowest ' + BattleStats[stat], 'bad'); + } + if (!poke.boosts[stat]) { + poke.boosts[stat] = 0; + } + poke.boosts[stat] += amount; + + if (!kwArgs.silent && kwArgs.from) { + let effect = Tools.getEffect(kwArgs.from); + let ofpoke = this.getPokemon(kwArgs.of); + if (!(effect.id === 'weakarmor' && stat === 'spe')) { + this.activateAbility(ofpoke || poke, effect); + } + } + this.scene.resultAnim(poke, poke.getBoost(stat), 'good'); + this.log(args, kwArgs); + break; + } + case '-unboost': { + let poke = this.getPokemon(args[1])!; + let stat = args[2] as BoostStatName; + if (this.gen === 1 && stat === 'spd') break; + if (this.gen === 1 && stat === 'spa') stat = 'spc'; + let amount = parseInt(args[3], 10); + if (amount === 0) { + this.scene.resultAnim(poke, 'Lowest ' + BattleStats[stat], 'bad'); + this.log(args, kwArgs); + break; + } + if (!poke.boosts[stat]) { + poke.boosts[stat] = 0; + } + poke.boosts[stat] -= amount; + + if (!kwArgs.silent && kwArgs.from) { + let effect = Tools.getEffect(kwArgs.from); + let ofpoke = this.getPokemon(kwArgs.of); + this.activateAbility(ofpoke || poke, effect); + } + this.scene.resultAnim(poke, poke.getBoost(stat), 'bad'); + this.log(args, kwArgs); + break; + } + case '-setboost': { + let poke = this.getPokemon(args[1])!; + let stat = args[2] as BoostStatName; + let amount = parseInt(args[3], 10); + poke.boosts[stat] = amount; + this.scene.resultAnim(poke, poke.getBoost(stat), (amount > 0 ? 'good' : 'bad')); + this.log(args, kwArgs); + break; + } + case '-swapboost': { + let poke = this.getPokemon(args[1])!; + let poke2 = this.getPokemon(args[2])!; + let stats = args[3] ? args[3].split(', ') : ['atk', 'def', 'spa', 'spd', 'spe', 'accuracy', 'evasion']; + let effect = Tools.getEffect(kwArgs.from); + for (let i = 0; i < stats.length; i++) { + let tmp = poke.boosts[stats[i]]; + poke.boosts[stats[i]] = poke2.boosts[stats[i]]; + if (!poke.boosts[stats[i]]) delete poke.boosts[stats[i]]; + poke2.boosts[stats[i]] = tmp; + if (!poke2.boosts[stats[i]]) delete poke2.boosts[stats[i]]; + } + this.scene.resultAnim(poke, 'Stats swapped', 'neutral'); + this.scene.resultAnim(poke2, 'Stats swapped', 'neutral'); + + this.log(args, kwArgs); + break; + } + case '-clearpositiveboost': { + let poke = this.getPokemon(args[1])!; + let ofpoke = this.getPokemon(args[2]); + let effect = Tools.getEffect(args[3]); + for (const stat in poke.boosts) { + if (poke.boosts[stat] > 0) delete poke.boosts[stat]; + } + this.scene.resultAnim(poke, 'Boosts lost', 'bad'); + + if (effect.id) { + switch (effect.id) { + case 'spectralthief': + // todo: update StealBoosts so it animates 1st on Spectral Thief + this.scene.runOtherAnim('spectralthiefboost' as ID, [ofpoke!, poke]); break; } - if (!poke.boosts[stat]) { - poke.boosts[stat] = 0; - } - poke.boosts[stat] -= amount; + } + this.log(args, kwArgs); + break; + } + case '-clearnegativeboost': { + let poke = this.getPokemon(args[1])!; + for (const stat in poke.boosts) { + if (poke.boosts[stat] < 0) delete poke.boosts[stat]; + } + this.scene.resultAnim(poke, 'Restored', 'good'); - let amountString = ''; - if (amount === 2) amountString = ' harshly'; - if (amount >= 3) amountString = ' severely'; - if (kwargs.silent) { - // do nothing - } else if (kwargs.from) { - let effect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of); - if (effect.effectType === 'Ability') { - this.scene.abilityActivateAnim(ofpoke || poke, effect.name); - this.message('', "[" + (ofpoke || poke).getName(true) + "'s " + effect.name + "!]"); - poke.markAbility(effect.name); - } - switch (effect.id) { - default: - if (effect.effectType === 'Ability') { - actions += "" + poke.getName() + "'s " + BattleStats[stat] + " fell" + amountString + "! "; - } - if (effect.effectType === 'Item') { - actions += "The " + effect.name + amountString + " lowered " + poke.getLowerName() + "'s " + BattleStats[stat] + "! "; - } - break; - } - } else { - actions += "" + poke.getName() + "'s " + BattleStats[stat] + " fell" + amountString + "! "; - } - this.scene.resultAnim(poke, poke.getBoost(stat), 'bad'); - break; - } case '-setboost': { - let poke = this.getPokemon(args[1])!; - let stat = args[2] as BoostStatName; - let amount = parseInt(args[3], 10); - let effect = Tools.getEffect(kwargs.from); - // let ofpoke = this.getPokemon(kwargs.of); - poke.boosts[stat] = amount; - this.scene.resultAnim(poke, poke.getBoost(stat), (amount > 0 ? 'good' : 'bad')); + this.log(args, kwArgs); + break; + } + case '-copyboost': { + let poke = this.getPokemon(args[1])!; + let frompoke = this.getPokemon(args[2])!; + let stats = args[3] ? args[3].split(', ') : ['atk', 'def', 'spa', 'spd', 'spe', 'accuracy', 'evasion']; + for (const stat of stats) { + poke.boosts[stat] = frompoke.boosts[stat]; + if (!poke.boosts[stat]) delete poke.boosts[stat]; + } - if (kwargs.silent) { - // do nothing - } else if (kwargs.from) { - switch (effect.id) { - case 'bellydrum': - actions += '' + poke.getName() + ' cut its own HP and maximized its Attack!'; - break; - case 'angerpoint': - this.scene.runOtherAnim('anger' as ID, [poke]); - this.scene.abilityActivateAnim(poke, 'Anger Point'); - this.message('', "[" + poke.getName(true) + "'s Anger Point!]"); - poke.markAbility('Anger Point'); - actions += '' + poke.getName() + ' maxed its Attack!'; - break; + this.log(args, kwArgs); + break; + } + case '-clearboost': { + let poke = this.getPokemon(args[1])!; + poke.boosts = {}; + this.scene.resultAnim(poke, 'Stats reset', 'neutral'); + + this.log(args, kwArgs); + break; + } + case '-invertboost': { + let poke = this.getPokemon(args[1])!; + for (const stat in poke.boosts) { + poke.boosts[stat] = -poke.boosts[stat]; + } + this.scene.resultAnim(poke, 'Stats inverted', 'neutral'); + + this.log(args, kwArgs); + break; + } + case '-clearallboost': { + for (const side of this.sides) { + for (const active of side.active) { + if (active) { + active.boosts = {}; + this.scene.resultAnim(active, 'Stats reset', 'neutral'); } } - break; - } case '-swapboost': { - let poke = this.getPokemon(args[1])!; - let poke2 = this.getPokemon(args[2])!; - let stats = args[3] ? args[3].split(', ') : ['atk', 'def', 'spa', 'spd', 'spe', 'accuracy', 'evasion']; - let effect = Tools.getEffect(kwargs.from); - for (let i = 0; i < stats.length; i++) { - let tmp = poke.boosts[stats[i]]; - poke.boosts[stats[i]] = poke2.boosts[stats[i]]; - if (!poke.boosts[stats[i]]) delete poke.boosts[stats[i]]; - poke2.boosts[stats[i]] = tmp; - if (!poke2.boosts[stats[i]]) delete poke2.boosts[stats[i]]; - } - this.scene.resultAnim(poke, 'Stats swapped', 'neutral'); - this.scene.resultAnim(poke2, 'Stats swapped', 'neutral'); + } - if (kwargs.silent) { - // do nothing - } else if (effect.id) { - switch (effect.id) { - case 'guardswap': - actions += '' + poke.getName() + ' switched all changes to its Defense and Sp. Def with its target!'; - break; - case 'heartswap': - actions += '' + poke.getName() + ' switched stat changes with its target!'; - break; - case 'powerswap': - actions += '' + poke.getName() + ' switched all changes to its Attack and Sp. Atk with its target!'; - break; - } + this.log(args, kwArgs); + break; + } + case '-crit': { + let poke = this.getPokemon(args[1]); + if (poke) this.scene.resultAnim(poke, 'Critical hit', 'bad'); + if (this.activeMoveIsSpread) kwArgs.spread = '.'; + this.log(args, kwArgs); + break; + } + case '-supereffective': { + let poke = this.getPokemon(args[1]); + if (poke) { + this.scene.resultAnim(poke, 'Super-effective', 'bad'); + if (window.Config && Config.server && Config.server.afd) { + this.scene.runOtherAnim('hitmark' as ID, [poke]); } + } + if (this.activeMoveIsSpread) kwArgs.spread = '.'; + this.log(args, kwArgs); + break; + } + case '-resisted': { + let poke = this.getPokemon(args[1]); + if (poke) this.scene.resultAnim(poke, 'Resisted', 'neutral'); + if (this.activeMoveIsSpread) kwArgs.spread = '.'; + this.log(args, kwArgs); + break; + } + case '-immune': { + let poke = this.getPokemon(args[1])!; + let effect = Tools.getEffect(args[2]); + let fromeffect = Tools.getEffect(kwArgs.from); + this.activateAbility(this.getPokemon(kwArgs.of) || poke, fromeffect); + this.log(args, kwArgs); + this.scene.resultAnim(poke, 'Immune', 'neutral'); + break; + } + case '-miss': { + let target = this.getPokemon(args[2]); + if (target) { + this.scene.resultAnim(target, 'Missed', 'neutral'); + } + this.log(args, kwArgs); + break; + } + case '-fail': { + let poke = this.getPokemon(args[1])!; + let effect = Tools.getEffect(args[2]); + let fromeffect = Tools.getEffect(kwArgs.from); + let ofpoke = this.getPokemon(kwArgs.of); + this.activateAbility(ofpoke || poke, fromeffect); + switch (effect.id) { + case 'brn': + this.scene.resultAnim(poke, 'Already burned', 'neutral'); break; - } case '-clearpositiveboost': { - let poke = this.getPokemon(args[1])!; - let ofpoke = this.getPokemon(args[2]); - let effect = Tools.getEffect(args[3]); - for (const stat in poke.boosts) { - if (poke.boosts[stat] > 0) delete poke.boosts[stat]; - } - this.scene.resultAnim(poke, 'Boosts lost', 'bad'); - - if (kwargs.silent) { - // do nothing - } else if (effect.id) { - switch (effect.id) { - case 'spectralthief': - // todo: update StealBoosts so it animates 1st on Spectral Thief - this.scene.runOtherAnim('spectralthiefboost' as ID, [ofpoke!, poke]); - actions += '' + ofpoke!.getName() + ' stole the target\'s boosted stats!'; - break; - } - } + case 'tox': + case 'psn': + this.scene.resultAnim(poke, 'Already poisoned', 'neutral'); break; - } case '-clearnegativeboost': { - let poke = this.getPokemon(args[1])!; - for (const stat in poke.boosts) { - if (poke.boosts[stat] < 0) delete poke.boosts[stat]; - } - this.scene.resultAnim(poke, 'Restored', 'good'); - - if (kwargs.silent) { - // do nothing - } else if (kwargs.zeffect) { - actions += '' + poke.getName() + ' returned its decreased stats to normal using its Z-Power!'; - break; - } - break; - } case '-copyboost': { - let poke = this.getPokemon(args[1])!; - let frompoke = this.getPokemon(args[2])!; - let stats = args[3] ? args[3].split(', ') : ['atk', 'def', 'spa', 'spd', 'spe', 'accuracy', 'evasion']; - // let effect = Tools.getEffect(kwargs.from); - for (let i = 0; i < stats.length; i++) { - poke.boosts[stats[i]] = frompoke.boosts[stats[i]]; - if (!poke.boosts[stats[i]]) delete poke.boosts[stats[i]]; - } - // poke.boosts = {...frompoke.boosts}; - - if (kwargs.silent) { - // do nothing + case 'slp': + if (fromeffect.id === 'uproar') { + this.scene.resultAnim(poke, 'Failed', 'neutral'); } else { - this.scene.resultAnim(poke, 'Stats copied', 'neutral'); - actions += "" + poke.getName() + " copied " + frompoke.getLowerName() + "'s stat changes!"; + this.scene.resultAnim(poke, 'Already asleep', 'neutral'); } break; - } case '-clearboost': { - let poke = this.getPokemon(args[1])!; - poke.boosts = {}; - this.scene.resultAnim(poke, 'Stats reset', 'neutral'); - - if (kwargs.silent) { - // do nothing - } else { - actions += '' + poke.getName() + '\'s stat changes were removed!'; - } + case 'par': + this.scene.resultAnim(poke, 'Already paralyzed', 'neutral'); break; - } case '-invertboost': { - let poke = this.getPokemon(args[1])!; - for (const stat in poke.boosts) { - poke.boosts[stat] = -poke.boosts[stat]; - } - this.scene.resultAnim(poke, 'Stats inverted', 'neutral'); - - if (kwargs.silent) { - // do nothing - } else { - actions += '' + poke.getName() + '\'s stat changes were inverted!'; - } + case 'frz': + this.scene.resultAnim(poke, 'Already frozen', 'neutral'); break; - } case '-clearallboost': { - for (const side of this.sides) { - for (const active of side.active) { - if (active) { - active.boosts = {}; - this.scene.resultAnim(active, 'Stats reset', 'neutral'); - } - } - } - - if (kwargs.silent) { - // do nothing - } else { - actions += 'All stat changes were eliminated!'; - } + case 'unboost': + this.scene.resultAnim(poke, 'Stat drop blocked', 'neutral'); break; - - } case '-crit': { - let poke = this.getPokemon(args[1]); - for (let j = 1; !poke && j < 10; j++) poke = this.getPokemon(minors[j][0][1]); - if (poke) this.scene.resultAnim(poke, 'Critical hit', 'bad'); - actions += "A critical hit" + (poke && this.activeMoveIsSpread ? " on " + poke.getLowerName() : "") + "! "; - if (window.Config && Config.server && Config.server.afd && !Config.server.afdCrit) { - actions += '
    Crit mattered? Buy Crit Insurance DLC, yours for only $4.99!
    CLICK HERE!
    '; - Config.server.afdCrit = true; - } - break; - - } case '-supereffective': { - let poke = this.getPokemon(args[1]); - for (let j = 1; !poke && j < 10; j++) poke = this.getPokemon(minors[j][0][1]); - if (poke) { - this.scene.resultAnim(poke, 'Super-effective', 'bad'); - if (window.Config && Config.server && Config.server.afd) { - this.scene.runOtherAnim('hitmark' as ID, [poke]); - } - } - actions += "It's super effective" + (poke && this.activeMoveIsSpread ? " on " + poke.getLowerName() : "") + "! "; - break; - - } case '-resisted': { - let poke = this.getPokemon(args[1]); - for (let j = 1; !poke && j < 10; j++) poke = this.getPokemon(minors[j][0][1]); - if (poke) this.scene.resultAnim(poke, 'Resisted', 'neutral'); - actions += "It's not very effective" + (poke && this.activeMoveIsSpread ? " on " + poke.getLowerName() : "..") + ". "; - break; - - } case '-immune': { - let poke = this.getPokemon(args[1])!; - let effect = Tools.getEffect(args[2]); - let fromeffect = Tools.getEffect(kwargs.from); - if (fromeffect && fromeffect.effectType === 'Ability') { - let ofpoke = this.getPokemon(kwargs.of) || poke; - this.scene.abilityActivateAnim(ofpoke, fromeffect.name); - this.message('', "[" + ofpoke.getName(true) + "'s " + fromeffect.name + "!]"); - ofpoke.markAbility(fromeffect.name); - } - if (effect.id == 'confusion') { - actions += "" + poke.getName() + " doesn't become confused! "; - } else if (kwargs.msg) { - actions += "It doesn't affect " + poke.getLowerName() + "... "; - } else if (kwargs.ohko) { - actions += "" + poke.getName() + " is unaffected! "; - } else { - actions += "It had no effect! "; - } - this.scene.resultAnim(poke, 'Immune', 'neutral'); - break; - - } case '-miss': { - let user = this.getPokemon(args[1])!; - let target = this.getPokemon(args[2]); - if (target) { - actions += "" + target.getName() + " avoided the attack!"; - this.scene.resultAnim(target, 'Missed', 'neutral'); - } else { - actions += "" + user.getName() + "'s attack missed!"; - } - break; - - } case '-fail': { - let poke = this.getPokemon(args[1])!; - let effect = Tools.getEffect(args[2]); - let fromeffect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of); + default: if (poke) { this.scene.resultAnim(poke, 'Failed', 'neutral'); } - // Sky Drop blocking moves takes priority over all other moves - if (fromeffect.id === 'skydrop') { - actions += "Sky Drop won't let " + poke.getLowerName() + " go!"; - break; - } - switch (effect.id) { - case 'brn': - this.scene.resultAnim(poke, 'Already burned', 'neutral'); - actions += "" + poke.getName() + " already has a burn."; - break; - case 'tox': - case 'psn': - this.scene.resultAnim(poke, 'Already poisoned', 'neutral'); - actions += "" + poke.getName() + " is already poisoned."; - break; - case 'slp': - if (fromeffect.id === 'uproar') { - this.scene.resultAnim(poke, 'Failed', 'neutral'); - if (kwargs.msg) { - actions += "But " + poke.getLowerName() + " can't sleep in an uproar!"; - } else { - actions += "But the uproar kept " + poke.getLowerName() + " awake!"; - } - } else { - this.scene.resultAnim(poke, 'Already asleep', 'neutral'); - actions += "" + poke.getName() + " is already asleep!"; - } - break; - case 'par': - this.scene.resultAnim(poke, 'Already paralyzed', 'neutral'); - actions += "" + poke.getName() + " is already paralyzed."; - break; - case 'frz': - this.scene.resultAnim(poke, 'Already frozen', 'neutral'); - actions += "" + poke.getName() + " is already frozen solid!"; - break; - case 'darkvoid': - case 'hyperspacefury': - if (kwargs.forme) { - actions += 'But ' + poke.getLowerName() + ' can\'t use it the way it is now!'; - } else { - actions += 'But ' + poke.getLowerName() + ' can\'t use the move!'; - } - break; - case 'magikarpsrevenge': - actions += 'But ' + poke.getLowerName() + ' can\'t use the move!'; - break; - case 'substitute': - if (kwargs.weak) { - actions += "But it does not have enough HP left to make a substitute!"; - } else { - actions += '' + poke.getName() + ' already has a substitute!'; - } - break; - case 'skydrop': - if (kwargs.heavy) { - actions += '' + poke.getName() + ' is too heavy to be lifted!'; - } else { - actions += "But it failed!"; - } - break; - case 'sunnyday': - case 'raindance': - case 'sandstorm': - case 'hail': - switch (fromeffect.id) { - case 'desolateland': - actions += "The extremely harsh sunlight was not lessened at all!"; - break; - case 'primordialsea': - actions += "There is no relief from this heavy rain!"; - break; - case 'deltastream': - actions += "The mysterious strong winds blow on regardless!"; - break; - default: - actions += "But it failed!"; - } - break; - case 'unboost': - if (fromeffect.effectType === 'Ability') { - this.scene.abilityActivateAnim(poke, fromeffect.name); - this.message('', "[" + poke.getName(true) + "'s " + fromeffect.name + "!]"); - poke.markAbility(fromeffect.name); - } else { - this.scene.resultAnim(poke, 'Stat drop blocked', 'neutral'); - } - switch (fromeffect.id) { - case 'flowerveil': - actions += '' + ofpoke!.getName() + ' surrounded itself with a veil of petals!'; - break; - default: - let stat = BattleLog.escapeHTML(args[3]); - actions += "" + poke.getName() + "'s " + (stat ? stat + " was" : "stats were") + " not lowered!"; - } - break; - default: - switch (fromeffect.id) { - case 'desolateland': - actions += "The Water-type attack evaporated in the harsh sunlight!"; - break; - case 'primordialsea': - actions += "The Fire-type attack fizzled out in the heavy rain!"; - break; - default: - actions += "But it failed!"; - } - break; + break; + } + this.log(args, kwArgs); + break; + } + case '-center': case '-notarget': case '-ohko': case '-nothing': + case '-combine': case '-hitcount': case '-waiting': case '-zbroken': { + this.log(args, kwArgs); + break; + } + case '-zpower': { + let poke = this.getPokemon(args[1])!; + this.scene.runOtherAnim('zpower' as ID, [poke]); + this.log(args, kwArgs); + break; + } + case '-prepare': { + let poke = this.getPokemon(args[1])!; + let moveid = toId(args[2]); + let target = this.getPokemon(args[3]) || poke.side.foe.active[0] || poke; + this.scene.runPrepareAnim(moveid, poke, target); + this.log(args, kwArgs); + break; + } + case '-mustrecharge': { + let poke = this.getPokemon(args[1])!; + poke.addMovestatus('mustrecharge' as ID); + this.scene.updateStatbar(poke); + break; + } + case '-status': { + let poke = this.getPokemon(args[1])!; + let effect = Tools.getEffect(kwArgs.from); + let ofpoke = this.getPokemon(kwArgs.of) || poke; + poke.status = args[2] as StatusName; + poke.removeVolatile('yawn' as ID); + this.activateAbility(ofpoke || poke, effect); + if (effect.effectType === 'Item') { + ofpoke.item = effect.name; + } + + switch (args[2]) { + case 'brn': + this.scene.resultAnim(poke, 'Burned', 'brn'); + this.scene.runStatusAnim('brn' as ID, [poke]); + break; + case 'tox': + this.scene.resultAnim(poke, 'Toxic poison', 'psn'); + this.scene.runStatusAnim('psn' as ID, [poke]); + poke.statusData.toxicTurns = (effect.name === "Toxic Orb" ? -1 : 0); + break; + case 'psn': + this.scene.resultAnim(poke, 'Poisoned', 'psn'); + this.scene.runStatusAnim('psn' as ID, [poke]); + break; + case 'slp': + this.scene.resultAnim(poke, 'Asleep', 'slp'); + if (effect.id === 'rest') { + poke.statusData.sleepTurns = 0; // for Gen 2 use through Sleep Talk } break; - - } case '-notarget': { - if (this.gen >= 5) { - actions += "But it failed!"; - } else { - actions += "But there was no target..."; - } + case 'par': + this.scene.resultAnim(poke, 'Paralyzed', 'par'); + this.scene.runStatusAnim('par' as ID, [poke]); break; - - } case '-ohko': { - actions += "It's a one-hit KO!"; + case 'frz': + this.scene.resultAnim(poke, 'Frozen', 'frz'); + this.scene.runStatusAnim('frz' as ID, [poke]); break; - - } case '-hitcount': { - let hits = parseInt(args[2], 10); - actions += 'Hit ' + hits + (hits > 1 ? ' times!' : ' time!'); - break; - - } case '-nothing': { - actions += "But nothing happened! "; - break; - - } case '-waiting': { - let poke = this.getPokemon(args[1])!; - let ofpoke = this.getPokemon(args[2])!; - actions += "" + poke.getName() + " is waiting for " + ofpoke.getLowerName() + "'s move..."; - break; - - } case '-combine': { - actions += "The two moves have become one! It's a combined move!"; - break; - - } case '-zpower': { - if (!this.hasPreMoveMessage && this.waitForResult()) return; - let poke = this.getPokemon(args[1])!; - this.scene.runOtherAnim('zpower' as ID, [poke]); - actions += "" + poke.getName() + " surrounded itself with its Z-Power! "; - this.hasPreMoveMessage = true; - break; - - } case '-zbroken': { - let poke = this.getPokemon(args[1])!; - actions += "" + poke.getName() + " couldn't fully protect itself and got hurt!"; - break; - - } case '-prepare': { - let poke = this.getPokemon(args[1])!; - let moveid = toId(args[2]); - let target = this.getPokemon(args[3]) || poke.side.foe.active[0] || poke; - this.scene.runPrepareAnim(moveid, poke, target); - break; - - } case '-mustrecharge': { - let poke = this.getPokemon(args[1])!; - poke.addMovestatus('mustrecharge' as ID); + default: this.scene.updateStatbar(poke); break; + } + this.log(args, kwArgs); + break; + } + case '-curestatus': { + let poke = this.getPokemon(args[1])!; + let effect = Tools.getEffect(kwArgs.from); - } case '-status': { - let poke = this.getPokemon(args[1])!; - let effect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of) || poke; - poke.status = args[2] as StatusName; - poke.removeVolatile('yawn' as ID); - let effectMessage = ""; - if (effect.effectType === 'Ability') { - this.scene.abilityActivateAnim(ofpoke, effect.name); - this.message('', "[" + ofpoke.getName(true) + "'s " + effect.name + "!]"); - ofpoke.markAbility(effect.name); - } else if (effect.effectType === 'Item') { - ofpoke.item = effect.name; - effectMessage = " by the " + effect.name; + if (effect.id) { + switch (effect.id) { + case 'flamewheel': + case 'flareblitz': + case 'fusionflare': + case 'sacredfire': + case 'scald': + case 'steameruption': + kwArgs.thaw = '.'; + break; } - + } + if (poke) { + poke.status = ''; switch (args[2]) { case 'brn': - this.scene.resultAnim(poke, 'Burned', 'brn'); - this.scene.runStatusAnim('brn' as ID, [poke]); - actions += "" + poke.getName() + " was burned" + effectMessage + "!"; + this.scene.resultAnim(poke, 'Burn cured', 'good'); break; case 'tox': - this.scene.resultAnim(poke, 'Toxic poison', 'psn'); - this.scene.runStatusAnim('psn' as ID, [poke]); - poke.statusData.toxicTurns = (effect.name === "Toxic Orb" ? -1 : 0); - actions += "" + poke.getName() + " was badly poisoned" + effectMessage + "!"; - break; case 'psn': - this.scene.resultAnim(poke, 'Poisoned', 'psn'); - this.scene.runStatusAnim('psn' as ID, [poke]); - actions += "" + poke.getName() + " was poisoned!"; + poke.statusData.toxicTurns = 0; + this.scene.resultAnim(poke, 'Poison cured', 'good'); break; case 'slp': - this.scene.resultAnim(poke, 'Asleep', 'slp'); - if (effect.id === 'rest') { - poke.statusData.sleepTurns = 0; // for Gen 2 use through Sleep Talk - actions += '' + poke.getName() + ' slept and became healthy!'; - } else { - actions += "" + poke.getName() + " fell asleep!"; - } + this.scene.resultAnim(poke, 'Woke up', 'good'); + poke.statusData.sleepTurns = 0; break; case 'par': - this.scene.resultAnim(poke, 'Paralyzed', 'par'); - this.scene.runStatusAnim('par' as ID, [poke]); - actions += "" + poke.getName() + " is paralyzed! It may be unable to move!"; + this.scene.resultAnim(poke, 'Paralysis cured', 'good'); break; case 'frz': - this.scene.resultAnim(poke, 'Frozen', 'frz'); - this.scene.runStatusAnim('frz' as ID, [poke]); - actions += "" + poke.getName() + " was frozen solid!"; + this.scene.resultAnim(poke, 'Thawed', 'good'); break; default: + poke.removeVolatile('confusion' as ID); + this.scene.resultAnim(poke, 'Cured', 'good'); + } + } + this.log(args, kwArgs); + break; + + } + case '-cureteam': { // For old gens when the whole team was always cured + let poke = this.getPokemon(args[1])!; + for (const target of poke.side.pokemon) { + target.status = ''; + this.scene.updateStatbarIfExists(target); + } + + this.scene.resultAnim(poke, 'Team Cured', 'good'); + this.log(args, kwArgs); + break; + } + case '-item': { + let poke = this.getPokemon(args[1])!; + let item = Tools.getItem(args[2]); + let effect = Tools.getEffect(kwArgs.from); + let ofpoke = this.getPokemon(kwArgs.of); + poke.item = item.name; + poke.itemEffect = ''; + poke.removeVolatile('airballoon' as ID); + if (item.id === 'airballoon') poke.addVolatile('airballoon' as ID); + + if (effect.id) { + switch (effect.id) { + case 'pickup': + this.activateAbility(poke, "Pickup"); + // falls through + case 'recycle': + poke.itemEffect = 'found'; + this.scene.resultAnim(poke, item.name, 'neutral'); + break; + case 'frisk': + this.activateAbility(ofpoke!, "Frisk"); + if (poke && poke !== ofpoke) { // used for gen 6 + poke.itemEffect = 'frisked'; + this.scene.resultAnim(poke, item.name, 'neutral'); + } + break; + case 'magician': + case 'pickpocket': + this.activateAbility(poke, effect.name); + // falls through + case 'thief': + case 'covet': + // simulate the removal of the item from the ofpoke + ofpoke!.item = ''; + ofpoke!.itemEffect = ''; + ofpoke!.prevItem = item.name; + ofpoke!.prevItemEffect = 'stolen'; + ofpoke!.addVolatile('itemremoved' as ID); + poke.itemEffect = 'stolen'; + this.scene.resultAnim(poke, item.name, 'neutral'); + this.scene.resultAnim(ofpoke!, 'Item Stolen', 'bad'); + break; + case 'harvest': + poke.itemEffect = 'harvested'; + this.activateAbility(poke, "Harvest"); + this.scene.resultAnim(poke, item.name, 'neutral'); + break; + case 'bestow': + poke.itemEffect = 'bestowed'; + this.scene.resultAnim(poke, item.name, 'neutral'); + break; + case 'trick': + poke.itemEffect = 'tricked'; + // falls through + default: + break; + } + } else { + switch (item.id) { + case 'airballoon': + this.scene.resultAnim(poke, 'Balloon', 'good'); + break; + } + } + this.log(args, kwArgs); + break; + } + case '-enditem': { + let poke = this.getPokemon(args[1])!; + let item = Tools.getItem(args[2]); + let effect = Tools.getEffect(kwArgs.from); + let ofpoke = this.getPokemon(kwArgs.of); + poke.item = ''; + poke.itemEffect = ''; + poke.prevItem = item.name; + poke.prevItemEffect = ''; + poke.removeVolatile('airballoon' as ID); + poke.addVolatile('itemremoved' as ID); + if (kwArgs.eat) { + poke.prevItemEffect = 'eaten'; + this.scene.runOtherAnim('consume' as ID, [poke]); + this.lastMove = item.id; + } else if (kwArgs.weaken) { + poke.prevItemEffect = 'eaten'; + this.lastMove = item.id; + } else if (effect.id) { + switch (effect.id) { + case 'fling': + poke.prevItemEffect = 'flung'; + break; + case 'knockoff': + poke.prevItemEffect = 'knocked off'; + this.scene.runOtherAnim('itemoff' as ID, [poke]); + this.scene.resultAnim(poke, 'Item knocked off', 'neutral'); + break; + case 'stealeat': + poke.prevItemEffect = 'stolen'; + break; + case 'gem': + poke.prevItemEffect = 'consumed'; + break; + case 'incinerate': + poke.prevItemEffect = 'incinerated'; + break; + } + } else { + switch (item.id) { + case 'airballoon': + poke.prevItemEffect = 'popped'; + poke.removeVolatile('airballoon' as ID); + this.scene.resultAnim(poke, 'Balloon popped', 'neutral'); + break; + case 'focussash': + poke.prevItemEffect = 'consumed'; + this.scene.resultAnim(poke, 'Sash', 'neutral'); + break; + case 'focusband': + this.scene.resultAnim(poke, 'Focus Band', 'neutral'); + break; + case 'redcard': + poke.prevItemEffect = 'held up'; + break; + default: + poke.prevItemEffect = 'consumed'; + break; + } + } + this.log(args, kwArgs); + break; + } + case '-ability': { + let poke = this.getPokemon(args[1])!; + let ability = Tools.getAbility(args[2]); + let effect = Tools.getEffect(kwArgs.from); + let ofpoke = this.getPokemon(kwArgs.of); + poke.rememberAbility(ability.name, effect.id && !kwArgs.fail); + + if (kwArgs.silent) { + // do nothing + } else if (effect.id) { + switch (effect.id) { + case 'trace': + this.activateAbility(poke, "Trace"); + this.scene.wait(500); + this.activateAbility(poke, ability.name, true); + ofpoke!.rememberAbility(ability.name); + break; + case 'powerofalchemy': + case 'receiver': + this.activateAbility(poke, effect.name); + this.scene.wait(500); + this.activateAbility(poke, ability.name, true); + ofpoke!.rememberAbility(ability.name); + break; + case 'roleplay': + this.activateAbility(poke, ability.name, true); + ofpoke!.rememberAbility(ability.name); + break; + case 'desolateland': + case 'primordialsea': + case 'deltastream': + if (kwArgs.fail) { + this.activateAbility(poke, ability.name); + } + break; + default: + this.activateAbility(poke, ability.name); + break; + } + } else { + this.activateAbility(poke, ability.name); + } + this.log(args, kwArgs); + break; + } + case '-endability': { + // deprecated; use |-start| for Gastro Acid + // and the third arg of |-ability| for Entrainment et al + let poke = this.getPokemon(args[1])!; + let ability = Tools.getAbility(args[2]); + poke.ability = '(suppressed)'; + + if (ability.id) { + if (!poke.baseAbility) poke.baseAbility = ability.name; + } + this.log(args, kwArgs); + break; + } + case 'detailschange': { + let poke = this.getPokemon(args[1])!; + poke.removeVolatile('formechange' as ID); + poke.removeVolatile('typeadd' as ID); + poke.removeVolatile('typechange' as ID); + + let newSpecies = args[2]; + let commaIndex = newSpecies.indexOf(','); + if (commaIndex !== -1) { + let level = newSpecies.substr(commaIndex + 1).trim(); + if (level.charAt(0) === 'L') { + poke.level = parseInt(level.substr(1), 10); + } + newSpecies = args[2].substr(0, commaIndex); + } + let template = Tools.getTemplate(newSpecies); + + poke.species = newSpecies; + poke.ability = poke.baseAbility = (template.abilities ? template.abilities['0'] : ''); + poke.weightkg = template.weightkg; + + poke.details = args[2]; + poke.searchid = args[1].substr(0, 2) + args[1].substr(3) + '|' + args[2]; + + this.scene.animTransform(poke, true, true); + this.log(args, kwArgs); + break; + } + case '-transform': { + let poke = this.getPokemon(args[1])!; + let tpoke = this.getPokemon(args[2])!; + let effect = Tools.getEffect(kwArgs.from); + + if (!kwArgs.silent) { + this.activateAbility(poke, effect); + } + + poke.boosts = {...tpoke.boosts}; + poke.copyTypesFrom(tpoke); + poke.weightkg = tpoke.weightkg; + poke.ability = tpoke.ability; + const species = (tpoke.volatiles.formechange ? tpoke.volatiles.formechange[1] : tpoke.species); + const pokemon = tpoke; + const shiny = tpoke.shiny; + const gender = tpoke.gender; + poke.addVolatile('transform' as ID, pokemon, shiny, gender); + poke.addVolatile('formechange' as ID, species); + for (const trackedMove of tpoke.moveTrack) { + poke.rememberMove(trackedMove[0], 0); + } + this.scene.animTransform(poke); + this.scene.resultAnim(poke, 'Transformed', 'good'); + this.log(['-transform', args[1], args[2], tpoke.species], kwArgs); + break; + } + case '-formechange': { + let poke = this.getPokemon(args[1])!; + let template = Tools.getTemplate(args[2]); + let fromeffect = Tools.getEffect(kwArgs.from); + let isCustomAnim = false; + poke.removeVolatile('typeadd' as ID); + poke.removeVolatile('typechange' as ID); + if (this.gen >= 7) poke.removeVolatile('autotomize' as ID); + + if (!kwArgs.silent) { + this.activateAbility(poke, fromeffect); + } + poke.addVolatile('formechange' as ID, template.species); // the formechange volatile reminds us to revert the sprite change on switch-out + this.scene.animTransform(poke, isCustomAnim); + this.log(args, kwArgs); + break; + } + case '-mega': { + let poke = this.getPokemon(args[1])!; + let item = Tools.getItem(args[3]); + if (args[3]) { + poke.item = item.name; + } + this.log(args, kwArgs); + break; + } + case '-primal': case '-burst': { + this.log(args, kwArgs); + break; + } + case '-start': { + let poke = this.getPokemon(args[1])!; + let effect = Tools.getEffect(args[2]); + let ofpoke = this.getPokemon(kwArgs.of); + let fromeffect = Tools.getEffect(kwArgs.from); + + this.activateAbility(poke, effect); + this.activateAbility(ofpoke || poke, fromeffect); + switch (effect.id) { + case 'typechange': + const types = BattleLog.escapeHTML(args[3]); + poke.removeVolatile('typeadd' as ID); + poke.addVolatile('typechange' as ID, types); + if (kwArgs.silent) { this.scene.updateStatbar(poke); break; } + this.scene.resultAnim(poke, types.split('/').map(function (type) { + return '' + type + ''; + }).join(' '), 'neutral'); break; - - } case '-curestatus': { - let poke = this.getPokemon(args[1])!; - let effect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of); - let pokeName, pokeSideN; - if (poke) { - poke.status = ''; - pokeName = poke.getName(); - pokeSideN = poke.side.n; - } else { - let parseIdResult = this.parsePokemonId(args[1]); - pokeName = parseIdResult.name; - pokeSideN = parseIdResult.siden; - } - if (args[2] === 'slp') poke.statusData.sleepTurns = 0; - if (effect.id === 'naturalcure' && !this.hasPreMoveMessage && this.waitForResult()) return; - - if (kwargs.silent) { - // do nothing - } else if (effect.id) { - switch (effect.id) { - case 'psychoshift': - actions += '' + pokeName + ' moved its status onto ' + ofpoke!.getLowerName() + '!'; - if (poke) this.scene.resultAnim(poke, 'Cured', 'good'); - break; - case 'flamewheel': - case 'flareblitz': - case 'fusionflare': - case 'sacredfire': - case 'scald': - case 'steameruption': - if (poke) this.scene.resultAnim(poke, 'Thawed', 'good'); - actions += "" + pokeName + "'s " + effect.name + " melted the ice!"; - break; - case 'naturalcure': - actions += "(" + pokeName + "'s Natural Cure activated!)"; - if (poke) poke.markAbility('Natural Cure'); - this.hasPreMoveMessage = true; - break; - default: - if (poke) this.scene.resultAnim(poke, 'Cured', 'good'); - actions += "" + pokeName + "'s " + effect.name + " heals its status!"; - break; - } - } else { - switch (args[2]) { - case 'brn': - if (poke) this.scene.resultAnim(poke, 'Burn cured', 'good'); - if (effect.effectType === 'Item') { - actions += "" + pokeName + "'s " + effect.name + " healed its burn!"; - break; - } - if (pokeSideN === 0) actions += "" + pokeName + "'s burn was healed."; - else actions += "" + pokeName + " healed its burn!"; - break; - case 'tox': - case 'psn': - if (poke) poke.statusData.toxicTurns = 0; - if (poke) this.scene.resultAnim(poke, 'Poison cured', 'good'); - if (effect.effectType === 'Item') { - actions += "" + pokeName + "'s " + effect.name + " cured its poison!"; - break; - } - actions += "" + pokeName + " was cured of its poisoning."; - break; - case 'slp': - if (poke) this.scene.resultAnim(poke, 'Woke up', 'good'); - if (poke) poke.statusData.sleepTurns = 0; - if (effect.effectType === 'Item') { - actions += "" + pokeName + "'s " + effect.name + " woke it up!"; - break; - } - actions += "" + pokeName + " woke up!"; - break; - case 'par': - if (poke) this.scene.resultAnim(poke, 'Paralysis cured', 'good'); - if (effect.effectType === 'Item') { - actions += "" + pokeName + "'s " + effect.name + " cured its paralysis!"; - break; - } - actions += "" + pokeName + " was cured of paralysis."; - break; - case 'frz': - if (poke) this.scene.resultAnim(poke, 'Thawed', 'good'); - if (effect.effectType === 'Item') { - actions += "" + pokeName + "'s " + effect.name + " defrosted it!"; - break; - } - actions += "" + pokeName + " thawed out!"; - break; - default: - if (poke) poke.removeVolatile('confusion' as ID); - if (poke) this.scene.resultAnim(poke, 'Cured', 'good'); - actions += "" + pokeName + "'s status cleared!"; - } + case 'typeadd': + const type = BattleLog.escapeHTML(args[3]); + poke.addVolatile('typeadd' as ID, type); + if (kwArgs.silent) break; + this.scene.resultAnim(poke, '' + type + '', 'neutral'); + break; + case 'powertrick': + this.scene.resultAnim(poke, 'Power Trick', 'neutral'); + break; + case 'foresight': + case 'miracleeye': + this.scene.resultAnim(poke, 'Identified', 'bad'); + break; + case 'telekinesis': + this.scene.resultAnim(poke, 'Telekinesis', 'neutral'); + break; + case 'confusion': + if (!kwArgs.already) { + this.scene.runStatusAnim('confused' as ID, [poke]); + this.scene.resultAnim(poke, 'Confused', 'bad'); } break; - - } case '-cureteam': { // For old gens when the whole team was always cured - let poke = this.getPokemon(args[1])!; - for (const target of poke.side.pokemon) { - target.status = ''; - this.scene.updateStatbarIfExists(target); - } - - this.scene.resultAnim(poke, 'Team Cured', 'good'); - let effect = Tools.getEffect(kwargs.from); - switch (effect.id) { - case 'aromatherapy': - actions += 'A soothing aroma wafted through the area!'; - break; - case 'healbell': - actions += 'A bell chimed!'; - break; - default: - actions += "" + poke.getName() + "'s team was cured!"; - break; - } - break; - - } case '-item': { - let poke = this.getPokemon(args[1])!; - let item = Tools.getItem(args[2]); - let effect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of); - poke.item = item.name; - poke.itemEffect = ''; - poke.removeVolatile('airballoon' as ID); - if (item.id === 'airballoon') poke.addVolatile('airballoon' as ID); - - if (effect.id) { - switch (effect.id) { - case 'pickup': - this.scene.abilityActivateAnim(poke, 'Pickup'); - this.message('', "[" + poke.getName(true) + "'s Pickup!]"); - poke.markAbility('Pickup'); - // falls through - case 'recycle': - poke.itemEffect = 'found'; - actions += '' + poke.getName() + ' found one ' + item.name + '!'; - this.scene.resultAnim(poke, item.name, 'neutral'); - break; - case 'frisk': - this.scene.abilityActivateAnim(ofpoke!, 'Frisk'); - this.message('', "[" + ofpoke!.getName(true) + "'s Frisk!]"); - ofpoke!.markAbility('Frisk'); - if (kwargs.identify) { // used for gen 6 - poke.itemEffect = 'frisked'; - actions += '' + ofpoke!.getName() + ' frisked ' + poke.getLowerName() + ' and found its ' + item.name + '!'; - this.scene.resultAnim(poke, item.name, 'neutral'); - } else { - actions += '' + ofpoke!.getName() + ' frisked its target and found one ' + item.name + '!'; - } - break; - case 'magician': - case 'pickpocket': - this.scene.abilityActivateAnim(poke, effect.name); - this.message('', "[" + poke.getName(true) + "'s " + effect.name + "!]"); - poke.markAbility(effect.name); - // falls through - case 'thief': - case 'covet': - // simulate the removal of the item from the ofpoke - ofpoke!.item = ''; - ofpoke!.itemEffect = ''; - ofpoke!.prevItem = item.name; - ofpoke!.prevItemEffect = 'stolen'; - ofpoke!.addVolatile('itemremoved' as ID); - poke.itemEffect = 'stolen'; - actions += '' + poke.getName() + ' stole ' + ofpoke!.getLowerName() + "'s " + item.name + "!"; - this.scene.resultAnim(poke, item.name, 'neutral'); - this.scene.resultAnim(ofpoke!, 'Item Stolen', 'bad'); - break; - case 'harvest': - poke.itemEffect = 'harvested'; - this.scene.abilityActivateAnim(poke, 'Harvest'); - this.message('', "[" + poke.getName(true) + "'s Harvest!]"); - poke.markAbility('Harvest'); - actions += '' + poke.getName() + ' harvested one ' + item.name + '!'; - this.scene.resultAnim(poke, item.name, 'neutral'); - break; - case 'bestow': - poke.itemEffect = 'bestowed'; - actions += '' + poke.getName() + ' received ' + item.name + ' from ' + ofpoke!.getLowerName() + '!'; - this.scene.resultAnim(poke, item.name, 'neutral'); - break; - case 'trick': - poke.itemEffect = 'tricked'; - // falls through - default: - actions += '' + poke.getName() + ' obtained one ' + item.name + '.'; - this.scene.resultAnim(poke, item.name, 'neutral'); - break; - } - } else { - switch (item.id) { - case 'airballoon': - this.scene.resultAnim(poke, 'Balloon', 'good'); - actions += "" + poke.getName() + " floats in the air with its Air Balloon!"; - break; - default: - actions += "" + poke.getName() + " has " + item.name + "!"; - break; - } - } - break; - - } case '-enditem': { - let poke = this.getPokemon(args[1])!; - let item = Tools.getItem(args[2]); - let effect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of); - poke.item = ''; - poke.itemEffect = ''; - poke.prevItem = item.name; - poke.prevItemEffect = ''; - poke.removeVolatile('airballoon' as ID); - poke.addVolatile('itemremoved' as ID); - if (kwargs.silent) { - // do nothing - } else if (kwargs.eat) { - poke.prevItemEffect = 'eaten'; - this.scene.runOtherAnim('consume' as ID, [poke]); - actions += '' + poke.getName() + ' ate its ' + item.name + '!'; - this.lastMove = item.id; - } else if (kwargs.weaken) { - poke.prevItemEffect = 'eaten'; - actions += 'The ' + item.name + ' weakened the damage to ' + poke.getLowerName() + '!'; - this.lastMove = item.id; - } else if (effect.id) { - switch (effect.id) { - case 'fling': - poke.prevItemEffect = 'flung'; - actions += "" + poke.getName() + ' flung its ' + item.name + '!'; - break; - case 'knockoff': - poke.prevItemEffect = 'knocked off'; - actions += '' + ofpoke!.getName() + ' knocked off ' + poke.getLowerName() + '\'s ' + item.name + '!'; - this.scene.runOtherAnim('itemoff' as ID, [poke]); - this.scene.resultAnim(poke, 'Item knocked off', 'neutral'); - break; - case 'stealeat': - poke.prevItemEffect = 'stolen'; - actions += '' + ofpoke!.getName() + ' stole and ate its target\'s ' + item.name + '!'; - break; - case 'gem': - poke.prevItemEffect = 'consumed'; - actions += 'The ' + item.name + ' strengthened ' + Tools.getMove(kwargs.move).name + '\'s power!'; - break; - case 'incinerate': - poke.prevItemEffect = 'incinerated'; - actions += "" + poke.getName() + "'s " + item.name + " was burned up!"; - break; - default: - actions += "" + poke.getName() + ' lost its ' + item.name + '!'; - break; - } - } else { - switch (item.id) { - case 'airballoon': - poke.prevItemEffect = 'popped'; - poke.removeVolatile('airballoon' as ID); - this.scene.resultAnim(poke, 'Balloon popped', 'neutral'); - actions += "" + poke.getName() + "'s Air Balloon popped!"; - break; - case 'focussash': - poke.prevItemEffect = 'consumed'; - this.scene.resultAnim(poke, 'Sash', 'neutral'); - actions += "" + poke.getName() + ' hung on using its Focus Sash!'; - break; - case 'focusband': - this.scene.resultAnim(poke, 'Focus Band', 'neutral'); - actions += "" + poke.getName() + ' hung on using its Focus Band!'; - break; - case 'powerherb': - poke.prevItemEffect = 'consumed'; - actions += "" + poke.getName() + " became fully charged due to its Power Herb!"; - break; - case 'whiteherb': - poke.prevItemEffect = 'consumed'; - actions += "" + poke.getName() + " returned its status to normal using its White Herb!"; - break; - case 'ejectbutton': - poke.prevItemEffect = 'consumed'; - actions += "" + poke.getName() + " is switched out with the Eject Button!"; - break; - case 'redcard': - poke.prevItemEffect = 'held up'; - actions += "" + poke.getName() + " held up its Red Card against " + ofpoke!.getLowerName() + "!"; - break; - default: - poke.prevItemEffect = 'consumed'; - actions += "" + poke.getName() + "'s " + item.name + " activated!"; - break; - } - } - break; - - } case '-ability': { - let poke = this.getPokemon(args[1])!; - let ability = Tools.getAbility(args[2]); - let effect = Tools.getEffect(kwargs.from); - let ofpoke = this.getPokemon(kwargs.of); - poke.markAbility(ability.name, effect.id && !kwargs.fail); - - if (kwargs.silent) { - // do nothing - } else if (effect.id) { - switch (effect.id) { - case 'trace': - this.scene.abilityActivateAnim(poke, "Trace"); - this.scene.wait(500); - this.scene.abilityActivateAnim(poke, ability.name); - this.message('', "[" + poke.getName(true) + "'s Trace!]"); - if (!poke.baseAbility) poke.baseAbility = effect.name; - ofpoke!.markAbility(ability.name); - actions += '' + poke.getName() + ' traced ' + ofpoke!.getLowerName() + '\'s ' + ability.name + '!'; - break; - case 'powerofalchemy': - case 'receiver': - this.scene.abilityActivateAnim(poke, effect.name); - this.scene.wait(500); - this.scene.abilityActivateAnim(poke, ability.name); - this.message('', "[" + poke.getName(true) + "'s " + effect.name + "!]"); - if (!poke.baseAbility) poke.baseAbility = effect.name; - actions += '' + ofpoke!.getName() + '\'s ' + ability.name + ' was taken over!'; - break; - case 'roleplay': - this.scene.abilityActivateAnim(poke, ability.name); - actions += '' + poke.getName() + ' copied ' + ofpoke!.getLowerName() + '\'s ' + ability.name + ' Ability!'; - ofpoke!.markAbility(ability.name); - break; - case 'desolateland': - if (kwargs.fail) { - this.scene.abilityActivateAnim(poke, ability.name); - this.message('', "[" + poke.getName(true) + "'s " + ability.name + "!]"); - actions += "The extremely harsh sunlight was not lessened at all!"; - } - break; - case 'primordialsea': - if (kwargs.fail) { - this.scene.abilityActivateAnim(poke, ability.name); - this.message('', "[" + poke.getName(true) + "'s " + ability.name + "!]"); - actions += "There's no relief from this heavy rain!"; - } - break; - case 'deltastream': - if (kwargs.fail) { - this.scene.abilityActivateAnim(poke, ability.name); - this.message('', "[" + poke.getName(true) + "'s " + ability.name + "!]"); - actions += "The mysterious strong winds blow on regardless!"; - } - break; - default: - this.scene.abilityActivateAnim(poke, ability.name); - actions += "" + poke.getName() + " acquired " + ability.name + "!"; - break; - } - } else { - this.scene.abilityActivateAnim(poke, ability.name); - this.message('', "[" + poke.getName(true) + "'s " + ability.name + "!]"); - switch (ability.id) { - case 'airlock': - case 'cloudnine': - actions += "The effects of the weather disappeared."; - break; - case 'anticipation': - actions += "" + poke.getName() + " shuddered!"; - break; - case 'aurabreak': - actions += "" + poke.getName() + " reversed all other Pokémon's auras!"; - break; - case 'comatose': - actions += "" + poke.getName() + " is drowsing!"; - break; - case 'darkaura': - actions += "" + poke.getName() + " is radiating a dark aura!"; - break; - case 'fairyaura': - actions += "" + poke.getName() + " is radiating a fairy aura!"; - break; - case 'moldbreaker': - actions += "" + poke.getName() + " breaks the mold!"; - break; - case 'pressure': - actions += "" + poke.getName() + " is exerting its pressure!"; - break; - case 'sturdy': - actions += "" + poke.getName() + " endured the hit!"; - break; - case 'teravolt': - actions += "" + poke.getName() + " is radiating a bursting aura!"; - break; - case 'turboblaze': - actions += "" + poke.getName() + " is radiating a blazing aura!"; - break; - case 'unnerve': - actions += "" + this.getSide(args[3]).getTeamName() + " is too nervous to eat Berries!"; - break; - default: - // Do nothing - } - } - break; - - } case '-endability': { - let poke = this.getPokemon(args[1])!; - let ability = Tools.getAbility(args[2]); - // let effect = Tools.getEffect(kwargs.from); - poke.ability = '(suppressed)'; - - if (kwargs.silent) { - // do nothing - } else if (ability.exists) { - actions += "(" + poke.getName() + "'s " + ability.name + " was removed.)"; - this.scene.resultAnim(poke, ability.name + ' removed', 'bad'); - if (!poke.baseAbility) poke.baseAbility = ability.name; - } else { - actions += "" + poke.getName() + "\'s Ability was suppressed!"; - } - break; - - } case '-transform': { - let poke = this.getPokemon(args[1])!; - let tpoke = this.getPokemon(args[2])!; - let effect = Tools.getEffect(kwargs.from); - - if (!kwargs.silent && effect.effectType === 'Ability') { - this.scene.abilityActivateAnim(poke, effect.name); - this.message('', "[" + poke.getName(true) + "'s " + effect.name + "!]"); - poke.markAbility(effect.name); - } - - actions += '' + poke.getName() + ' transformed into ' + tpoke.species + '!'; - poke.boosts = {...tpoke.boosts}; - poke.copyTypesFrom(tpoke); - poke.weightkg = tpoke.weightkg; - poke.ability = tpoke.ability; - const species = (tpoke.volatiles.formechange ? tpoke.volatiles.formechange[1] : tpoke.species); - const pokemon = tpoke; - const shiny = tpoke.shiny; - const gender = tpoke.gender; - poke.addVolatile('transform' as ID, pokemon, shiny, gender); - poke.addVolatile('formechange' as ID, species); - for (const trackedMove of tpoke.moveTrack) { - poke.markMove(trackedMove[0], 0); - } - this.scene.animTransform(poke); - this.scene.resultAnim(poke, 'Transformed', 'good'); - break; - } case '-formechange': { - let poke = this.getPokemon(args[1])!; - let template = Tools.getTemplate(args[2]); - let fromeffect = Tools.getEffect(kwargs.from); - let isCustomAnim = false; - poke.removeVolatile('typeadd' as ID); - poke.removeVolatile('typechange' as ID); - if (this.gen >= 7) poke.removeVolatile('autotomize' as ID); - - if (kwargs.silent) { - // do nothing - } else { - if (fromeffect.effectType === 'Ability') { - this.scene.abilityActivateAnim(poke, fromeffect.name); - this.message('', "[" + poke.getName(true) + "'s " + fromeffect.name + "!]"); - poke.markAbility(fromeffect.name); - } - if (kwargs.msg) { - actions += "" + poke.getName() + " transformed!"; - if (toId(template.species) === 'shaymin') break; - } else if (toId(template.species) === 'darmanitanzen') { - actions += "Zen Mode triggered!"; - } else if (toId(template.species) === 'darmanitan') { - actions += "Zen Mode ended!"; - } else if (toId(template.species) === 'aegislashblade') { - actions += "Changed to Blade Forme!"; - } else if (toId(template.species) === 'aegislash') { - actions += "Changed to Shield Forme!"; - } else if (toId(template.species) === 'wishiwashischool') { - actions += "" + poke.getName() + " formed a school!"; - isCustomAnim = true; - } else if (toId(template.species) === 'wishiwashi') { - actions += "" + poke.getName() + " stopped schooling!"; - isCustomAnim = true; - } else if (toId(template.species) === 'miniormeteor') { - actions += "Shields Down deactivated!"; - } else if (toId(template.species) === 'minior') { - actions += "Shields Down activated!"; - } - } - poke.addVolatile('formechange' as ID, template.species); // the formechange volatile reminds us to revert the sprite change on switch-out - this.scene.animTransform(poke, isCustomAnim); - break; - } case '-mega': { - let poke = this.getPokemon(args[1])!; - let item = Tools.getItem(args[3]); - if (args[2] === 'Rayquaza') { - actions += "" + BattleLog.escapeHTML(poke.side.name) + "'s fervent wish has reached " + poke.getLowerName() + "!"; - } else { - poke.item = item.name; - actions += "" + poke.getName() + "'s " + item.name + " is reacting to " + (this.gen >= 7 ? "the Key Stone" : BattleLog.escapeHTML(poke.side.name) + "'s Mega Bracelet") + "!"; - } - actions += "
    " + poke.getName() + " has Mega Evolved into Mega " + args[2] + "!"; - break; - } case '-primal': { - let poke = this.getPokemon(args[1])!; - actions += "" + poke.getName() + "'s Primal Reversion! It reverted to its primal state!"; - break; - } case '-burst': { - let poke = this.getPokemon(args[1])!; - actions += "Bright light is about to burst out of " + poke.getLowerName() + "!"; - break; - - } case '-start': { - let poke = this.getPokemon(args[1])!; - let effect = Tools.getEffect(args[2]); - let ofpoke = this.getPokemon(kwargs.of); - let fromeffect = Tools.getEffect(kwargs.from); - if (fromeffect.id === 'protean' && !this.hasPreMoveMessage && this.waitForResult()) return; - - if (effect.effectType === 'Ability') { - this.scene.abilityActivateAnim(poke, effect.name); - this.message('', "[" + poke.getName(true) + "'s " + effect.name + "!]"); - poke.markAbility(effect.name); - } - if (kwargs.silent && effect.id !== 'typechange' && effect.id !== 'typeadd') { - // do nothing - } else { - switch (effect.id) { - case 'typechange': - const types = BattleLog.escapeHTML(args[3]); - poke.removeVolatile('typeadd' as ID); - poke.addVolatile('typechange' as ID, types); - if (kwargs.silent) { - this.scene.updateStatbar(poke); - break; - } - if (fromeffect.id) { - if (fromeffect.id === 'colorchange' || fromeffect.id === 'protean') { - this.scene.abilityActivateAnim(poke, fromeffect.name); - this.message('', "[" + poke.getName(true) + "'s " + fromeffect.name + "!]"); - poke.markAbility(fromeffect.name); - actions += "" + poke.getName() + " transformed into the " + types + " type!"; - this.hasPreMoveMessage = true; - } else if (fromeffect.id === 'reflecttype') { - poke.copyTypesFrom(ofpoke!); - if (!kwargs.silent) actions += "" + poke.getName() + "'s type became the same as " + ofpoke!.getLowerName() + "'s type!"; - } else if (fromeffect.id === 'burnup') { - actions += "" + poke.getName() + " burned itself out!"; - } else if (!kwargs.silent) { - actions += "" + poke.getName() + "'s " + fromeffect.name + " made it the " + types + " type!"; - } - } else { - actions += "" + poke.getName() + " transformed into the " + types + " type!"; - } - this.scene.resultAnim(poke, types.split('/').map(function (type) { - return '' + type + ''; - }).join(' '), 'neutral'); - break; - case 'typeadd': - const type = BattleLog.escapeHTML(args[3]); - poke.addVolatile('typeadd' as ID, type); - if (kwargs.silent) break; - actions += "" + type + " type was added to " + poke.getLowerName() + "!"; - this.scene.resultAnim(poke, '' + type + '', 'neutral'); - break; - case 'powertrick': - this.scene.resultAnim(poke, 'Power Trick', 'neutral'); - actions += "" + poke.getName() + " switched its Attack and Defense!"; - break; - case 'foresight': - case 'miracleeye': - this.scene.resultAnim(poke, 'Identified', 'bad'); - actions += "" + poke.getName() + " was identified!"; - break; - case 'telekinesis': - this.scene.resultAnim(poke, 'Telekinesis', 'neutral'); - actions += "" + poke.getName() + " was hurled into the air!"; - break; - case 'confusion': - if (kwargs.already) { - actions += "" + poke.getName() + " is already confused!"; - } else { - this.scene.runStatusAnim('confused' as ID, [poke]); - this.scene.resultAnim(poke, 'Confused', 'bad'); - if (kwargs.fatigue) { - actions += "" + poke.getName() + " became confused due to fatigue!"; - } else { - actions += "" + poke.getName() + " became confused!"; - } - } - break; - case 'leechseed': - this.scene.updateStatbar(poke); - actions += '' + poke.getName() + ' was seeded!'; - break; - case 'healblock': - this.scene.resultAnim(poke, 'Heal Block', 'bad'); - actions += "" + poke.getName() + " was prevented from healing!"; - break; - case 'mudsport': - this.scene.resultAnim(poke, 'Mud Sport', 'neutral'); - actions += "Electricity's power was weakened!"; - break; - case 'watersport': - this.scene.resultAnim(poke, 'Water Sport', 'neutral'); - actions += "Fire's power was weakened!"; - break; - case 'yawn': - this.scene.resultAnim(poke, 'Drowsy', 'slp'); - actions += "" + poke.getName() + ' grew drowsy!'; - break; - case 'flashfire': - actions += 'The power of ' + poke.getLowerName() + '\'s Fire-type moves rose!'; - break; - case 'taunt': - this.scene.resultAnim(poke, 'Taunted', 'bad'); - actions += '' + poke.getName() + ' fell for the taunt!'; - break; - case 'imprison': - this.scene.resultAnim(poke, 'Imprisoning', 'good'); - actions += "" + poke.getName() + " sealed any moves its target shares with it!"; - break; - case 'disable': - if (fromeffect.effectType === 'Ability') { - this.scene.abilityActivateAnim(ofpoke!, fromeffect.name); - this.message('', "[" + ofpoke!.getName(true) + "'s " + fromeffect.name + "!]"); - ofpoke!.markAbility(fromeffect.name); - } - this.scene.resultAnim(poke, 'Disabled', 'bad'); - actions += "" + poke.getName() + "'s " + BattleLog.escapeHTML(args[3]) + " was disabled!"; - break; - case 'embargo': - this.scene.resultAnim(poke, 'Embargo', 'bad'); - actions += "" + poke.getName() + " can't use items anymore!"; - break; - case 'torment': - this.scene.resultAnim(poke, 'Tormented', 'bad'); - actions += '' + poke.getName() + ' was subjected to torment!'; - break; - case 'ingrain': - this.scene.resultAnim(poke, 'Ingrained', 'good'); - actions += '' + poke.getName() + ' planted its roots!'; - break; - case 'aquaring': - this.scene.resultAnim(poke, 'Aqua Ring', 'good'); - actions += '' + poke.getName() + ' surrounded itself with a veil of water!'; - break; - case 'stockpile1': - this.scene.resultAnim(poke, 'Stockpile', 'good'); - actions += '' + poke.getName() + ' stockpiled 1!'; - break; - case 'stockpile2': - poke.removeVolatile('stockpile1' as ID); - this.scene.resultAnim(poke, 'Stockpile×2', 'good'); - actions += '' + poke.getName() + ' stockpiled 2!'; - break; - case 'stockpile3': - poke.removeVolatile('stockpile2' as ID); - this.scene.resultAnim(poke, 'Stockpile×3', 'good'); - actions += '' + poke.getName() + ' stockpiled 3!'; - break; - case 'perish0': - poke.removeVolatile('perish1' as ID); - actions += '' + poke.getName() + "'s perish count fell to 0."; - break; - case 'perish1': - poke.removeVolatile('perish2' as ID); - this.scene.resultAnim(poke, 'Perish next turn', 'bad'); - actions += '' + poke.getName() + "'s perish count fell to 1."; - break; - case 'perish2': - poke.removeVolatile('perish3' as ID); - this.scene.resultAnim(poke, 'Perish in 2', 'bad'); - actions += '' + poke.getName() + "'s perish count fell to 2."; - break; - case 'perish3': - this.scene.resultAnim(poke, 'Perish in 3', 'bad'); - actions += '' + poke.getName() + "'s perish count fell to 3."; - break; - case 'encore': - this.scene.resultAnim(poke, 'Encored', 'bad'); - actions += '' + poke.getName() + ' received an encore!'; - break; - case 'bide': - this.scene.resultAnim(poke, 'Bide', 'good'); - actions += "" + poke.getName() + " is storing energy!"; - break; - case 'slowstart': - actions += "" + poke.getName() + " can't get it going!"; - break; - case 'attract': - if (fromeffect.effectType === 'Ability') { - this.scene.abilityActivateAnim(ofpoke!, fromeffect.name); - this.message('', "[" + ofpoke!.getName(true) + "'s " + fromeffect.name + "!]"); - ofpoke!.markAbility(fromeffect.name); - } - this.scene.resultAnim(poke, 'Attracted', 'bad'); - if (fromeffect.effectType === 'Item') { - actions += "" + poke.getName() + " fell in love from the " + fromeffect.name + "!"; - } else { - actions += "" + poke.getName() + " fell in love!"; - } - break; - case 'autotomize': - this.scene.resultAnim(poke, 'Lightened', 'good'); - actions += "" + poke.getName() + " became nimble!"; - break; - case 'focusenergy': - this.scene.resultAnim(poke, '+Crit rate', 'good'); - if (fromeffect.effectType === 'Item') { - actions += "" + poke.getName() + " used the " + fromeffect.name + " to get pumped!"; - } else if (kwargs.zeffect) { - actions += "" + poke.getName() + " boosted its critical-hit ratio using its Z-Power!"; - } else { - actions += "" + poke.getName() + " is getting pumped!"; - } - break; - case 'curse': - this.scene.resultAnim(poke, 'Cursed', 'bad'); - actions += "" + ofpoke!.getName() + " cut its own HP and put a curse on " + poke.getLowerName() + "!"; - break; - case 'nightmare': - this.scene.resultAnim(poke, 'Nightmare', 'bad'); - actions += "" + poke.getName() + " began having a nightmare!"; - break; - case 'magnetrise': - this.scene.resultAnim(poke, 'Magnet Rise', 'good'); - actions += "" + poke.getName() + " levitated with electromagnetism!"; - break; - case 'smackdown': - this.scene.resultAnim(poke, 'Smacked Down', 'bad'); - actions += "" + poke.getName() + " fell straight down!"; - poke.removeVolatile('magnetrise' as ID); - poke.removeVolatile('telekinesis' as ID); - if (poke.lastMove === 'fly' || poke.lastMove === 'bounce') this.scene.animReset(poke); - break; - case 'substitute': - if (kwargs.damage) { - this.scene.resultAnim(poke, 'Damage', 'bad'); - actions += "The substitute took damage for " + poke.getLowerName() + "!"; - } else if (kwargs.block) { - this.scene.resultAnim(poke, 'Blocked', 'neutral'); - actions += 'But it failed!'; - } else if (kwargs.already) { - actions += '' + poke.getName() + ' already has a substitute!'; - } else { - actions += '' + poke.getName() + ' put in a substitute!'; - } - break; - case 'uproar': - if (kwargs.upkeep) { - actions += "" + poke.getName() + " is making an uproar!"; - } else { - actions += "" + poke.getName() + " caused an uproar!"; - } - break; - case 'doomdesire': - actions += '' + poke.getName() + ' chose Doom Desire as its destiny!'; - break; - case 'futuresight': - actions += '' + poke.getName() + ' foresaw an attack!'; - break; - case 'mimic': - actions += '' + poke.getName() + ' learned ' + BattleLog.escapeHTML(args[3]) + '!'; - break; - case 'laserfocus': - actions += '' + poke.getName() + ' concentrated intensely!'; - break; - case 'followme': - case 'ragepowder': // Deprecated, now uses -singleturn - actions += '' + poke.getName() + ' became the center of attention!'; - break; - case 'powder': // Deprecated, now uses -singleturn - actions += '' + poke.getName() + ' is covered in powder!'; - break; - - // Gen 1 - case 'lightscreen': - this.scene.resultAnim(poke, 'Light Screen', 'good'); - actions += '' + poke.getName() + '\'s protected against special attacks!'; - break; - case 'reflect': - this.scene.resultAnim(poke, 'Reflect', 'good'); - actions += '' + poke.getName() + ' gained armor!'; - break; - - default: - actions += "" + poke.getName() + "'s " + effect.name + " started!"; - } - } - poke.addVolatile(effect.id); + case 'leechseed': this.scene.updateStatbar(poke); break; - } case '-end': { - let poke = this.getPokemon(args[1])!; - let effect = Tools.getEffect(args[2]); - let fromeffect = Tools.getEffect(kwargs.from); - poke.removeVolatile(effect.id); + case 'healblock': + this.scene.resultAnim(poke, 'Heal Block', 'bad'); + break; + case 'yawn': + this.scene.resultAnim(poke, 'Drowsy', 'slp'); + break; + case 'taunt': + this.scene.resultAnim(poke, 'Taunted', 'bad'); + break; + case 'imprison': + this.scene.resultAnim(poke, 'Imprisoning', 'good'); + case 'disable': + this.scene.resultAnim(poke, 'Disabled', 'bad'); + break; + case 'embargo': + this.scene.resultAnim(poke, 'Embargo', 'bad'); + break; + case 'torment': + this.scene.resultAnim(poke, 'Tormented', 'bad'); + break; + case 'ingrain': + this.scene.resultAnim(poke, 'Ingrained', 'good'); + break; + case 'aquaring': + this.scene.resultAnim(poke, 'Aqua Ring', 'good'); + break; + case 'stockpile1': + this.scene.resultAnim(poke, 'Stockpile', 'good'); + break; + case 'stockpile2': + poke.removeVolatile('stockpile1' as ID); + this.scene.resultAnim(poke, 'Stockpile×2', 'good'); + break; + case 'stockpile3': + poke.removeVolatile('stockpile2' as ID); + this.scene.resultAnim(poke, 'Stockpile×3', 'good'); + break; + case 'perish0': + poke.removeVolatile('perish1' as ID); + break; + case 'perish1': + poke.removeVolatile('perish2' as ID); + this.scene.resultAnim(poke, 'Perish next turn', 'bad'); + break; + case 'perish2': + poke.removeVolatile('perish3' as ID); + this.scene.resultAnim(poke, 'Perish in 2', 'bad'); + break; + case 'perish3': + this.scene.resultAnim(poke, 'Perish in 3', 'bad'); + break; + case 'encore': + this.scene.resultAnim(poke, 'Encored', 'bad'); + break; + case 'bide': + this.scene.resultAnim(poke, 'Bide', 'good'); + break; + case 'attract': + this.scene.resultAnim(poke, 'Attracted', 'bad'); + break; + case 'autotomize': + this.scene.resultAnim(poke, 'Lightened', 'good'); + break; + case 'focusenergy': + this.scene.resultAnim(poke, '+Crit rate', 'good'); + break; + case 'curse': + this.scene.resultAnim(poke, 'Cursed', 'bad'); + break; + case 'nightmare': + this.scene.resultAnim(poke, 'Nightmare', 'bad'); + break; + case 'magnetrise': + this.scene.resultAnim(poke, 'Magnet Rise', 'good'); + break; + case 'smackdown': + this.scene.resultAnim(poke, 'Smacked Down', 'bad'); + poke.removeVolatile('magnetrise' as ID); + poke.removeVolatile('telekinesis' as ID); + if (poke.lastMove === 'fly' || poke.lastMove === 'bounce') this.scene.animReset(poke); + break; + case 'substitute': + if (kwArgs.damage) { + this.scene.resultAnim(poke, 'Damage', 'bad'); + } else if (kwArgs.block) { + this.scene.resultAnim(poke, 'Blocked', 'neutral'); + } + break; - if (kwargs.silent) { - // do nothing - } else { - switch (effect.id) { - case 'powertrick': - this.scene.resultAnim(poke, 'Power Trick', 'neutral'); - actions += "" + poke.getName() + " switched its Attack and Defense!"; - break; - case 'telekinesis': - this.scene.resultAnim(poke, 'Telekinesis ended', 'neutral'); - actions += "" + poke.getName() + " was freed from the telekinesis!"; - break; - case 'skydrop': - if (kwargs.interrupt) { - this.scene.anim(poke, {time: 100}); - } - actions += "" + poke.getName() + " was freed from the Sky Drop!"; - break; - case 'confusion': - this.scene.resultAnim(poke, 'Confusion ended', 'good'); - if (!kwargs.silent) { - if (fromeffect.effectType === 'Item') { - actions += "" + poke.getName() + "'s " + fromeffect.name + " snapped it out of its confusion!"; - break; - } - if (poke.side.n === 0) actions += "" + poke.getName() + " snapped out of its confusion."; - else actions += "" + poke.getName() + " snapped out of confusion!"; - } - break; - case 'leechseed': - if (fromeffect.id === 'rapidspin') { - this.scene.resultAnim(poke, 'De-seeded', 'good'); - actions += "" + poke.getName() + " was freed from Leech Seed!"; - } - break; - case 'healblock': - this.scene.resultAnim(poke, 'Heal Block ended', 'good'); - actions += "" + poke.getName() + "'s Heal Block wore off!"; - break; - case 'attract': - this.scene.resultAnim(poke, 'Attract ended', 'good'); - if (fromeffect.id === 'oblivious') { - actions += '' + poke.getName() + " got over its infatuation."; - } - if (fromeffect.id === 'mentalherb') { - actions += "" + poke.getName() + " cured its infatuation status using its " + fromeffect.name + "!"; - } - break; - case 'taunt': - this.scene.resultAnim(poke, 'Taunt ended', 'good'); - actions += '' + poke.getName() + "'s taunt wore off!"; - break; - case 'disable': - this.scene.resultAnim(poke, 'Disable ended', 'good'); - actions += '' + poke.getName() + "'s move is no longer disabled!"; - break; - case 'embargo': - this.scene.resultAnim(poke, 'Embargo ended', 'good'); - actions += "" + poke.getName() + " can use items again!"; - break; - case 'torment': - this.scene.resultAnim(poke, 'Torment ended', 'good'); - actions += '' + poke.getName() + "'s torment wore off!"; - break; - case 'encore': - this.scene.resultAnim(poke, 'Encore ended', 'good'); - actions += '' + poke.getName() + "'s encore ended!"; - break; - case 'bide': - this.scene.runOtherAnim('bideunleash' as ID, [poke]); - actions += "" + poke.getName() + " unleashed its energy!"; - break; - case 'illusion': - this.scene.resultAnim(poke, 'Illusion ended', 'bad'); - actions += "" + poke.getName() + "'s illusion wore off!"; - poke.markAbility('Illusion'); - break; - case 'slowstart': - this.scene.resultAnim(poke, 'Slow Start ended', 'good'); - actions += "" + poke.getName() + " finally got its act together!"; - break; - case 'magnetrise': - if (poke.side.n === 0) actions += "" + poke.getName() + "'s electromagnetism wore off!"; - else actions += "The electromagnetism of " + poke.getLowerName() + " wore off!"; - break; - case 'perishsong': // for backwards compatibility - poke.removeVolatile('perish3' as ID); - break; - case 'substitute': - this.scene.resultAnim(poke, 'Faded', 'bad'); - actions += '' + poke.getName() + "'s substitute faded!"; - break; - case 'uproar': - actions += "" + poke.getName() + " calmed down."; - break; - case 'stockpile': - poke.removeVolatile('stockpile1' as ID); - poke.removeVolatile('stockpile2' as ID); - poke.removeVolatile('stockpile3' as ID); - actions += "" + poke.getName() + "'s stockpiled effect wore off!"; - break; - case 'bind': - case 'wrap': - case 'clamp': - case 'whirlpool': - case 'firespin': - case 'magmastorm': - case 'sandtomb': - case 'infestation': - actions += '' + poke.getName() + ' was freed from ' + effect.name + '!'; - break; - default: - if (effect.effectType === 'Move') { - if (effect.name === 'Doom Desire') { - this.scene.runOtherAnim('doomdesirehit' as ID, [poke]); - } - if (effect.name === 'Future Sight') { - this.scene.runOtherAnim('futuresighthit' as ID, [poke]); - } - actions += '' + poke.getName() + " took the " + effect.name + " attack!"; - } else { - actions += "" + poke.getName() + "'s " + effect.name + " ended!"; - } + // Gen 1 + case 'lightscreen': + this.scene.resultAnim(poke, 'Light Screen', 'good'); + break; + case 'reflect': + this.scene.resultAnim(poke, 'Reflect', 'good'); + break; + } + poke.addVolatile(effect.id); + this.scene.updateStatbar(poke); + this.log(args, kwArgs); + break; + } + case '-end': { + let poke = this.getPokemon(args[1])!; + let effect = Tools.getEffect(args[2]); + let fromeffect = Tools.getEffect(kwArgs.from); + poke.removeVolatile(effect.id); + + if (kwArgs.silent) { + // do nothing + } else { + switch (effect.id) { + case 'powertrick': + this.scene.resultAnim(poke, 'Power Trick', 'neutral'); + break; + case 'telekinesis': + this.scene.resultAnim(poke, 'Telekinesis ended', 'neutral'); + break; + case 'skydrop': + if (kwArgs.interrupt) { + this.scene.anim(poke, {time: 100}); } - } - this.scene.updateStatbar(poke); - break; - } case '-singleturn': { - let poke = this.getPokemon(args[1])!; - let effect = Tools.getEffect(args[2]); - let ofpoke = this.getPokemon(kwargs.of); - // let fromeffect = Tools.getEffect(kwargs.from); - poke.addTurnstatus(effect.id); - - switch (effect.id) { - case 'roost': - this.scene.resultAnim(poke, 'Landed', 'neutral'); - //actions += '' + poke.getName() + ' landed on the ground!'; - break; - case 'quickguard': - this.scene.resultAnim(poke, 'Quick Guard', 'good'); - actions += "Quick Guard protected " + poke.side.getLowerTeamName() + "!"; - break; - case 'wideguard': - this.scene.resultAnim(poke, 'Wide Guard', 'good'); - actions += "Wide Guard protected " + poke.side.getLowerTeamName() + "!"; - break; - case 'craftyshield': - this.scene.resultAnim(poke, 'Crafty Shield', 'good'); - actions += "Crafty Shield protected " + poke.side.getLowerTeamName() + "!"; - break; - case 'matblock': - this.scene.resultAnim(poke, 'Mat Block', 'good'); - actions += '' + poke.getName() + ' intends to flip up a mat and block incoming attacks!'; - break; - case 'protect': - this.scene.resultAnim(poke, 'Protected', 'good'); - actions += '' + poke.getName() + ' protected itself!'; - break; - case 'endure': - this.scene.resultAnim(poke, 'Enduring', 'good'); - actions += '' + poke.getName() + ' braced itself!'; - break; - case 'helpinghand': - this.scene.resultAnim(poke, 'Helping Hand', 'good'); - actions += '' + ofpoke!.getName() + " is ready to help " + poke.getLowerName() + "!"; - break; - case 'focuspunch': - this.scene.resultAnim(poke, 'Focusing', 'neutral'); - actions += '' + poke.getName() + ' is tightening its focus!'; - poke.markMove(effect.name, 0); - break; - case 'shelltrap': - this.scene.resultAnim(poke, 'Trap set', 'neutral'); - actions += '' + poke.getName() + ' set a shell trap!'; - poke.markMove(effect.name, 0); - break; - case 'snatch': - actions += '' + poke.getName() + ' waits for a target to make a move!'; - break; - case 'magiccoat': - actions += '' + poke.getName() + ' shrouded itself with Magic Coat!'; - break; - case 'electrify': - actions += '' + poke.getName() + '\'s moves have been electrified!'; - break; - case 'followme': - case 'ragepowder': - case 'spotlight': - if (kwargs.zeffect) { - actions += '' + poke.getName() + ' became the center of attention using its Z-Power!'; - } else { - actions += '' + poke.getName() + ' became the center of attention!'; - } - break; - case 'powder': - actions += '' + poke.getName() + ' is covered in powder!'; - break; - case 'instruct': - actions += '' + poke.getName() + ' used the move instructed by ' + ofpoke!.getLowerName() + '!'; - break; - case 'beakblast': - this.scene.runOtherAnim('bidecharge' as ID, [poke]); - this.scene.resultAnim(poke, 'Beak Blast', 'neutral'); - actions += '' + poke.getName() + ' started heating up its beak!'; - break; - } - this.scene.updateStatbar(poke); - break; - } case '-singlemove': { - let poke = this.getPokemon(args[1])!; - let effect = Tools.getEffect(args[2]); - // let ofpoke = this.getPokemon(kwargs.of); - // let fromeffect = Tools.getEffect(kwargs.from); - poke.addMovestatus(effect.id); - - switch (effect.id) { - case 'grudge': - this.scene.resultAnim(poke, 'Grudge', 'neutral'); - actions += '' + poke.getName() + ' wants its target to bear a grudge!'; - break; - case 'destinybond': - this.scene.resultAnim(poke, 'Destiny Bond', 'neutral'); - actions += '' + poke.getName() + ' is hoping to take its attacker down with it!'; - break; - } - break; - - } case '-activate': { - let poke = this.getPokemon(args[1])!; - let effect = Tools.getEffect(args[2]); - let ofpoke = this.getPokemon(kwargs.of); - if ((effect.id === 'confusion' || effect.id === 'attract') && !this.hasPreMoveMessage && this.waitForResult()) return; - if (effect.effectType === 'Ability') { - this.scene.abilityActivateAnim(poke, effect.name); - this.message('', "[" + poke.getName(true) + "'s " + effect.name + "!]"); - poke.markAbility(effect.name); - } - switch (effect.id) { - case 'healreplacement': - actions += "" + poke.getName() + " will restore its replacement's HP using its Z-Power!"; break; case 'confusion': - actions += "" + poke.getName() + " is confused!"; - this.hasPreMoveMessage = true; + this.scene.resultAnim(poke, 'Confusion ended', 'good'); break; - case 'destinybond': - actions += '' + poke.getName() + ' took its attacker down with it!'; - break; - case 'snatch': - actions += "" + poke.getName() + " snatched " + ofpoke!.getLowerName() + "'s move!"; - break; - case 'grudge': - actions += "" + poke.getName() + "'s " + BattleLog.escapeHTML(args[3]) + " lost all of its PP due to the grudge!"; - poke.markMove(args[3], Infinity); - break; - case 'quickguard': - poke.addTurnstatus('quickguard' as ID); - this.scene.resultAnim(poke, 'Quick Guard', 'good'); - actions += "Quick Guard protected " + poke.getLowerName() + "!"; - break; - case 'wideguard': - poke.addTurnstatus('wideguard' as ID); - this.scene.resultAnim(poke, 'Wide Guard', 'good'); - actions += "Wide Guard protected " + poke.getLowerName() + "!"; - break; - case 'craftyshield': - poke.addTurnstatus('craftyshield' as ID); - this.scene.resultAnim(poke, 'Crafty Shield', 'good'); - actions += "Crafty Shield protected " + poke.getLowerName() + "!"; - break; - case 'protect': - poke.addTurnstatus('protect' as ID); - this.scene.resultAnim(poke, 'Protected', 'good'); - actions += '' + poke.getName() + ' protected itself!'; - break; - case 'substitute': - if (kwargs.damage) { - this.scene.resultAnim(poke, 'Damage', 'bad'); - actions += 'The substitute took damage for ' + poke.getLowerName() + '!'; - } else if (kwargs.block) { - this.scene.resultAnim(poke, 'Blocked', 'neutral'); - actions += '' + poke.getName() + "'s Substitute blocked " + Tools.getMove(kwargs.block || args[3]).name + '!'; + case 'leechseed': + if (fromeffect.id === 'rapidspin') { + this.scene.resultAnim(poke, 'De-seeded', 'good'); } break; + case 'healblock': + this.scene.resultAnim(poke, 'Heal Block ended', 'good'); + break; case 'attract': - this.scene.runStatusAnim('attracted' as ID, [poke]); - actions += '' + poke.getName() + ' is in love with ' + ofpoke!.getLowerName() + '!'; - this.hasPreMoveMessage = true; + this.scene.resultAnim(poke, 'Attract ended', 'good'); + break; + case 'taunt': + this.scene.resultAnim(poke, 'Taunt ended', 'good'); + break; + case 'disable': + this.scene.resultAnim(poke, 'Disable ended', 'good'); + break; + case 'embargo': + this.scene.resultAnim(poke, 'Embargo ended', 'good'); + break; + case 'torment': + this.scene.resultAnim(poke, 'Torment ended', 'good'); + break; + case 'encore': + this.scene.resultAnim(poke, 'Encore ended', 'good'); break; case 'bide': - this.scene.runOtherAnim('bidecharge' as ID, [poke]); - actions += "" + poke.getName() + " is storing energy!"; + this.scene.runOtherAnim('bideunleash' as ID, [poke]); break; - case 'mist': - actions += "" + poke.getName() + " is protected by the mist!"; + case 'illusion': + this.scene.resultAnim(poke, 'Illusion ended', 'bad'); + poke.rememberAbility('Illusion'); break; - case 'safeguard': - actions += "" + poke.getName() + " is protected by Safeguard!"; + case 'slowstart': + this.scene.resultAnim(poke, 'Slow Start ended', 'good'); break; - case 'trapped': - actions += "" + poke.getName() + " can no longer escape!"; + case 'perishsong': // for backwards compatibility + poke.removeVolatile('perish3' as ID); break; - case 'stickyweb': - actions += '' + poke.getName() + ' was caught in a sticky web!'; + case 'substitute': + this.scene.resultAnim(poke, 'Faded', 'bad'); break; - case 'happyhour': - actions += 'Everyone is caught up in the happy atmosphere!'; - break; - case 'celebrate': - actions += 'Congratulations, ' + BattleLog.escapeHTML(poke.side.name) + '!'; + case 'stockpile': + poke.removeVolatile('stockpile1' as ID); + poke.removeVolatile('stockpile2' as ID); + poke.removeVolatile('stockpile3' as ID); break; + default: + if (effect.effectType === 'Move') { + if (effect.name === 'Doom Desire') { + this.scene.runOtherAnim('doomdesirehit' as ID, [poke]); + } + if (effect.name === 'Future Sight') { + this.scene.runOtherAnim('futuresighthit' as ID, [poke]); + } + } + } + } + this.scene.updateStatbar(poke); + this.log(args, kwArgs); + break; + } + case '-singleturn': { + let poke = this.getPokemon(args[1])!; + let effect = Tools.getEffect(args[2]); + let ofpoke = this.getPokemon(kwArgs.of); + // let fromeffect = Tools.getEffect(kwArgs.from); + poke.addTurnstatus(effect.id); - // move activations - case 'aromatherapy': - this.scene.resultAnim(poke, 'Team Cured', 'good'); - actions += 'A soothing aroma wafted through the area!'; - break; - case 'healbell': - this.scene.resultAnim(poke, 'Team Cured', 'good'); - actions += 'A bell chimed!'; - break; - case 'trick': - case 'switcheroo': - actions += '' + poke.getName() + ' switched items with its target!'; - break; - case 'brickbreak': - actions += poke.getName() + " shattered " + ofpoke!.side.getTeamName() + " protections!"; - ofpoke!.side.removeSideCondition('Reflect'); - ofpoke!.side.removeSideCondition('LightScreen'); - break; - case 'beatup': - actions += "" + BattleLog.escapeHTML(kwargs.of) + "'s attack!"; - break; - case 'pursuit': - actions += "(" + poke.getName() + " is being withdrawn!)"; - break; - case 'hyperspacefury': - case 'hyperspacehole': - case 'phantomforce': - case 'shadowforce': - case 'feint': + switch (effect.id) { + case 'roost': + this.scene.resultAnim(poke, 'Landed', 'neutral'); + break; + case 'quickguard': + this.scene.resultAnim(poke, 'Quick Guard', 'good'); + break; + case 'wideguard': + this.scene.resultAnim(poke, 'Wide Guard', 'good'); + break; + case 'craftyshield': + this.scene.resultAnim(poke, 'Crafty Shield', 'good'); + break; + case 'matblock': + this.scene.resultAnim(poke, 'Mat Block', 'good'); + break; + case 'protect': + this.scene.resultAnim(poke, 'Protected', 'good'); + break; + case 'endure': + this.scene.resultAnim(poke, 'Enduring', 'good'); + break; + case 'helpinghand': + this.scene.resultAnim(poke, 'Helping Hand', 'good'); + break; + case 'focuspunch': + this.scene.resultAnim(poke, 'Focusing', 'neutral'); + poke.rememberMove(effect.name, 0); + break; + case 'shelltrap': + this.scene.resultAnim(poke, 'Trap set', 'neutral'); + poke.rememberMove(effect.name, 0); + break; + case 'beakblast': + this.scene.runOtherAnim('bidecharge' as ID, [poke]); + this.scene.resultAnim(poke, 'Beak Blast', 'neutral'); + break; + } + this.scene.updateStatbar(poke); + this.log(args, kwArgs); + break; + } + case '-singlemove': { + let poke = this.getPokemon(args[1])!; + let effect = Tools.getEffect(args[2]); + // let ofpoke = this.getPokemon(kwArgs.of); + // let fromeffect = Tools.getEffect(kwArgs.from); + poke.addMovestatus(effect.id); + + switch (effect.id) { + case 'grudge': + this.scene.resultAnim(poke, 'Grudge', 'neutral'); + break; + case 'destinybond': + this.scene.resultAnim(poke, 'Destiny Bond', 'neutral'); + break; + } + this.log(args, kwArgs); + break; + } + case '-activate': { + let poke = this.getPokemon(args[1])!; + let effect = Tools.getEffect(args[2]); + let ofpoke = this.getPokemon(kwArgs.of); + this.activateAbility(poke, effect); + switch (effect.id) { + case 'grudge': + poke.rememberMove(args[3], Infinity); + break; + case 'quickguard': + poke.addTurnstatus('quickguard' as ID); + this.scene.resultAnim(poke, 'Quick Guard', 'good'); + break; + case 'wideguard': + poke.addTurnstatus('wideguard' as ID); + this.scene.resultAnim(poke, 'Wide Guard', 'good'); + break; + case 'craftyshield': + poke.addTurnstatus('craftyshield' as ID); + this.scene.resultAnim(poke, 'Crafty Shield', 'good'); + break; + case 'protect': + poke.addTurnstatus('protect' as ID); + this.scene.resultAnim(poke, 'Protected', 'good'); + break; + case 'substitute': + if (kwArgs.damage) { + this.scene.resultAnim(poke, 'Damage', 'bad'); + } else if (kwArgs.block) { + this.scene.resultAnim(poke, 'Blocked', 'neutral'); + } + break; + case 'attract': + this.scene.runStatusAnim('attracted' as ID, [poke]); + break; + case 'bide': + this.scene.runOtherAnim('bidecharge' as ID, [poke]); + break; + + // move activations + case 'aromatherapy': + this.scene.resultAnim(poke, 'Team Cured', 'good'); + break; + case 'healbell': + this.scene.resultAnim(poke, 'Team Cured', 'good'); + break; + case 'brickbreak': + ofpoke!.side.removeSideCondition('Reflect'); + ofpoke!.side.removeSideCondition('LightScreen'); + break; + case 'hyperspacefury': + case 'hyperspacehole': + case 'phantomforce': + case 'shadowforce': + case 'feint': + this.scene.resultAnim(poke, 'Protection broken', 'bad'); + poke.removeTurnstatus('protect' as ID); + for (const target of poke.side.pokemon) { + target.removeTurnstatus('wideguard' as ID); + target.removeTurnstatus('quickguard' as ID); + target.removeTurnstatus('craftyshield' as ID); + target.removeTurnstatus('matblock' as ID); + this.scene.updateStatbar(target); + } + break; + case 'spite': + let move = Tools.getMove(args[3]).name; + let pp = BattleLog.escapeHTML(args[4]); + poke.rememberMove(move, Number(pp)); + break; + case 'gravity': + poke.removeVolatile('magnetrise' as ID); + poke.removeVolatile('telekinesis' as ID); + this.scene.anim(poke, {time: 100}); + break; + case 'skillswap': + if (this.gen <= 4) break; + let pokeability = BattleLog.escapeHTML(args[3]) || ofpoke!.ability; + let ofpokeability = BattleLog.escapeHTML(args[4]) || poke.ability; + if (pokeability) { + poke.ability = pokeability; + if (!ofpoke!.baseAbility) ofpoke!.baseAbility = pokeability; + } + if (ofpokeability) { + ofpoke!.ability = ofpokeability; + if (!poke.baseAbility) poke.baseAbility = ofpokeability; + } + if (poke.side !== ofpoke!.side) { + this.activateAbility(poke, pokeability, true); + this.activateAbility(ofpoke, ofpokeability, true); + } + break; + + // ability activations + case 'wonderguard': // Deprecated, now uses -immune + this.scene.resultAnim(poke, 'Immune', 'neutral'); + break; + case 'forewarn': + if (ofpoke) { + ofpoke.rememberMove(args[3], 0); + } else { + let foeActive = [] as Pokemon[]; + for (const target of poke.side.foe.active) if (target) foeActive.push(target); + if (foeActive.length === 1) { + foeActive[0].rememberMove(args[3], 0); + } + } + break; + case 'mummy': + if (!args[3]) break; // if Mummy activated but failed, no ability will have been sent + let ability = Tools.getAbility(args[3]); + this.activateAbility(ofpoke, ability.name); + this.activateAbility(poke, "Mummy"); + this.scene.wait(700); + this.activateAbility(ofpoke, "Mummy", true); + break; + + // item activations + case 'leppaberry': + case 'mysteryberry': + poke.rememberMove(args[3], effect.id === 'leppaberry' ? -10 : -5); + break; + case 'focusband': + poke.item = 'Focus Band'; + break; + case 'safetygoggles': + poke.item = 'Safety Goggles'; + break; + case 'protectivepads': + poke.item = 'Protective Pads'; + break; + default: + if (kwArgs.broken) { // for custom moves that break protection this.scene.resultAnim(poke, 'Protection broken', 'bad'); - if (kwargs.broken) { - actions += "It broke through " + poke.getLowerName() + "'s protection!"; - } else { - actions += "" + poke.getName() + " fell for the feint!"; - } - poke.removeTurnstatus('protect' as ID); - for (const target of poke.side.pokemon) { - target.removeTurnstatus('wideguard' as ID); - target.removeTurnstatus('quickguard' as ID); - target.removeTurnstatus('craftyshield' as ID); - target.removeTurnstatus('matblock' as ID); - this.scene.updateStatbar(target); - } - break; - case 'spite': - let move = Tools.getMove(args[3]).name; - let pp = BattleLog.escapeHTML(args[4]); - actions += "It reduced the PP of " + poke.getLowerName() + "'s " + move + " by " + pp + "!"; - poke.markMove(move, Number(pp)); - break; - case 'gravity': - actions += "" + poke.getName() + " couldn't stay airborne because of gravity!"; - poke.removeVolatile('magnetrise' as ID); - poke.removeVolatile('telekinesis' as ID); - this.scene.anim(poke, {time: 100}); - break; - case 'magnitude': - actions += "Magnitude " + BattleLog.escapeHTML(args[3]) + "!"; - break; - case 'sketch': - actions += "" + poke.getName() + " sketched " + BattleLog.escapeHTML(args[3]) + "!"; - break; - case 'skillswap': - actions += "" + poke.getName() + " swapped Abilities with its target!"; - if (this.gen <= 4) break; - let pokeability = BattleLog.escapeHTML(args[3]) || ofpoke!.ability; - let ofpokeability = BattleLog.escapeHTML(args[4]) || poke.ability; - if (pokeability) { - poke.ability = pokeability; - if (!ofpoke!.baseAbility) ofpoke!.baseAbility = pokeability; - } - if (ofpokeability) { - ofpoke!.ability = ofpokeability; - if (!poke.baseAbility) poke.baseAbility = ofpokeability; - } - if (poke.side !== ofpoke!.side) { - this.scene.abilityActivateAnim(poke, pokeability); - this.scene.abilityActivateAnim(ofpoke!, ofpokeability); - actions += "
    " + poke.getName() + " acquired " + pokeability + "!"; - actions += "
    " + ofpoke!.getName() + " acquired " + ofpokeability + "!"; - } - break; - case 'charge': - actions += "" + poke.getName() + " began charging power!"; - break; - case 'struggle': - actions += "" + poke.getName() + " has no moves left!"; - break; - case 'bind': - actions += '' + poke.getName() + ' was squeezed by ' + ofpoke!.getLowerName() + '!'; - break; - case 'wrap': - actions += '' + poke.getName() + ' was wrapped by ' + ofpoke!.getLowerName() + '!'; - break; - case 'clamp': - actions += '' + ofpoke!.getName() + ' clamped down on ' + poke.getLowerName() + '!'; - break; - case 'whirlpool': - actions += '' + poke.getName() + ' became trapped in the vortex!'; - break; - case 'firespin': - actions += '' + poke.getName() + ' became trapped in the fiery vortex!'; - break; - case 'magmastorm': - actions += '' + poke.getName() + ' became trapped by swirling magma!'; - break; - case 'sandtomb': - actions += '' + poke.getName() + ' became trapped by the quicksand!'; - break; - case 'infestation': - actions += '' + poke.getName() + ' has been afflicted with an infestation by ' + ofpoke!.getLowerName() + '!'; - break; - case 'afteryou': - actions += '' + poke.getName() + ' took the kind offer!'; - break; - case 'quash': - actions += "" + poke.getName() + "'s move was postponed!"; - break; - case 'powersplit': - actions += '' + poke.getName() + ' shared its power with the target!'; - break; - case 'guardsplit': - actions += '' + poke.getName() + ' shared its guard with the target!'; - break; - case 'speedswap': - actions += '' + poke.getName() + ' switched Speed with its target!'; - break; - case 'ingrain': - actions += '' + poke.getName() + ' anchored itself with its roots!'; - break; - case 'matblock': - actions += '' + BattleLog.escapeHTML(args[3]) + ' was blocked by the kicked-up mat!'; - break; - case 'powder': - actions += 'When the flame touched the powder on the Pokémon, it exploded!'; - break; - case 'fairylock': - actions += 'No one will be able to run away during the next turn!'; - break; - case 'lockon': - case 'mindreader': - actions += '' + poke.getName() + ' took aim at ' + ofpoke!.getLowerName() + '!'; - break; - case 'endure': - actions += '' + poke.getName() + ' endured the hit!'; - break; - case 'electricterrain': - actions += '' + poke.getName() + ' surrounds itself with electrified terrain!'; - break; - case 'mistyterrain': - actions += '' + poke.getName() + ' surrounds itself with a protective mist!'; - break; - case 'psychicterrain': - actions += '' + poke.getName() + ' surrounds itself with psychic terrain!'; - break; - - // ability activations - case 'magicbounce': - case 'magiccoat': - case 'rebound': - break; - case 'wonderguard': // Deprecated, now uses -immune - this.scene.resultAnim(poke, 'Immune', 'neutral'); - actions += '' + poke.getName() + '\'s Wonder Guard evades the attack!'; - break; - case 'forewarn': - if (this.gen >= 5) { - actions += "It was alerted to " + ofpoke!.getLowerName() + "'s " + BattleLog.escapeHTML(args[3]) + "!"; - ofpoke!.markMove(args[3], 0); - } else { - actions += "" + poke.getName() + "'s Forewarn alerted it to " + BattleLog.escapeHTML(args[3]) + "!"; - let foeActive = [] as Pokemon[]; - for (const target of poke.side.foe.active) if (target) foeActive.push(target); - if (foeActive.length === 1) { - foeActive[0].markMove(args[3], 0); - } - } - break; - case 'mummy': - if (!args[3]) break; // if Mummy activated but failed, no ability will have been sent - let ability = Tools.getAbility(args[3]); - this.scene.abilityActivateAnim(ofpoke!, ability.name); - this.scene.wait(700); - this.message('', "[" + ofpoke!.getName(true) + "'s " + ability.name + "!]"); - ofpoke!.markAbility(ability.name); - this.scene.abilityActivateAnim(ofpoke!, 'Mummy'); - this.message('', "[" + ofpoke!.getName(true) + "'s Mummy!]"); - ofpoke!.markAbility('Mummy', true); - actions += "" + ofpoke!.getName() + "'s Ability became Mummy!"; - break; - case 'anticipation': // Deprecated, now uses -ability. This is for replay compatability - actions += "" + poke.getName() + " shuddered!"; - break; - case 'lightningrod': - case 'stormdrain': - actions += '' + poke.getName() + ' took the attack!'; - break; - case 'telepathy': - actions += "" + poke.getName() + " avoids attacks by its ally Pokémon!"; - break; - case 'stickyhold': - actions += "" + poke.getName() + "'s item cannot be removed!"; - break; - case 'suctioncups': - actions += '' + poke.getName() + ' anchors itself!'; - break; - case 'symbiosis': - actions += '' + poke.getName() + ' shared its ' + Tools.getItem(args[3]).name + ' with ' + ofpoke!.getLowerName() + '!'; - break; - case 'aromaveil': - actions += '' + ofpoke!.getName() + ' is protected by an aromatic veil!'; - break; - case 'flowerveil': - actions += '' + ofpoke!.getName() + ' surrounded itself with a veil of petals!'; - break; - case 'sweetveil': - actions += '' + ofpoke!.getName() + ' surrounded itself with a veil of sweetness!'; - break; - case 'battlebond': - actions += '' + poke.getName() + ' became fully charged due to its bond with its Trainer!'; - break; - case 'disguise': - actions += 'Its disguise served it as a decoy!'; - break; - case 'powerconstruct': - actions += 'You sense the presence of many!'; - break; - case 'persistent': // CAP - actions += '' + poke.getName() + ' extends ' + Tools.getMove(args[3]).name + ' by 2 turns!'; - break; - - // weather activations - case 'deltastream': - actions += "The mysterious strong winds weakened the attack!"; - break; - - // item activations - case 'custapberry': - case 'quickclaw': - //actions += '' + poke.getName() + ' is already preparing its next move!'; - actions += '' + poke.getName() + '\'s ' + effect.name + ' let it move first!'; - break; - case 'leppaberry': - case 'mysteryberry': - actions += '' + poke.getName() + " restored PP to its " + BattleLog.escapeHTML(args[3]) + " move using " + effect.name + "!"; - poke.markMove(args[3], effect.id === 'leppaberry' ? -10 : -5); - break; - case 'focusband': - poke.item = 'Focus Band'; - actions += '' + poke.getName() + " hung on using its Focus Band!"; - break; - case 'safetygoggles': - poke.item = 'Safety Goggles'; - actions += '' + poke.getName() + " is not affected by " + BattleLog.escapeHTML(args[3]) + " thanks to its Safety Goggles!"; - break; - case 'protectivepads': - poke.item = 'Protective Pads'; - actions += '' + poke.getName() + " protected itself with the Protective Pads!"; - break; - default: - if (kwargs.broken) { // for custom moves that break protection - this.scene.resultAnim(poke, 'Protection broken', 'bad'); - actions += "It broke through " + poke.getLowerName() + "'s protection!"; - } else if (effect.effectType !== 'Ability') { - actions += "" + poke.getName() + "'s " + effect.name + " activated!"; - } } - break; - - } case '-sidestart': { - let side = this.getSide(args[1]); - let effect = Tools.getEffect(args[2]); - side.addSideCondition(effect); - - switch (effect.id) { - case 'stealthrock': - actions += "Pointed stones float in the air around " + side.getLowerTeamName() + "!"; - break; - case 'spikes': - actions += "Spikes were scattered on the ground all around " + side.getLowerTeamName() + "!"; - break; - case 'toxicspikes': - actions += "Poison spikes were scattered on the ground all around " + side.getLowerTeamName() + "!"; - break; - case 'stickyweb': - actions += "A sticky web spreads out on the ground around " + side.getLowerTeamName() + "!"; - break; - case 'tailwind': - actions += "The Tailwind blew from behind " + side.getLowerTeamName() + "!"; - this.scene.updateWeather(); - break; - case 'auroraveil': - actions += "Aurora Veil made " + side.getLowerTeamName() + " stronger against physical and special moves!"; - this.scene.updateWeather(); - break; - case 'reflect': - actions += "Reflect made " + side.getLowerTeamName() + " stronger against physical moves!"; - this.scene.updateWeather(); - break; - case 'lightscreen': - actions += "Light Screen made " + side.getLowerTeamName() + " stronger against special moves!"; - this.scene.updateWeather(); - break; - case 'safeguard': - actions += "" + side.getTeamName() + " cloaked itself in a mystical veil!"; - this.scene.updateWeather(); - break; - case 'mist': - actions += "" + side.getTeamName() + " became shrouded in mist!"; - this.scene.updateWeather(); - break; - case 'luckychant': - actions += 'Lucky Chant shielded ' + side.getLowerTeamName() + ' from critical hits!'; - break; - case 'firepledge': - actions += "A sea of fire enveloped " + side.getLowerTeamName() + "!"; - break; - case 'waterpledge': - actions += "A rainbow appeared in the sky on " + side.getLowerTeamName() + "'s side!"; - break; - case 'grasspledge': - actions += "A swamp enveloped " + side.getLowerTeamName() + "!"; - break; - default: - actions += "" + effect.name + " started!"; - break; - } - break; - } case '-sideend': { - let side = this.getSide(args[1]); - let effect = Tools.getEffect(args[2]); - // let from = Tools.getEffect(kwargs.from); - // let ofpoke = this.getPokemon(kwargs.of); - side.removeSideCondition(effect.name); - - switch (effect.id) { - case 'stealthrock': - actions += "The pointed stones disappeared from around " + side.getLowerTeamName() + "!"; - break; - case 'spikes': - actions += "The spikes disappeared from the ground around " + side.getLowerTeamName() + "!"; - break; - case 'toxicspikes': - actions += "The poison spikes disappeared from the ground around " + side.getLowerTeamName() + "!"; - break; - case 'stickyweb': - actions += "The sticky web has disappeared from the ground around " + side.getLowerTeamName() + "!"; - break; - case 'tailwind': - actions += "" + side.getTeamName() + "'s Tailwind petered out!"; - break; - case 'auroraveil': - actions += "" + side.getTeamName() + "'s Aurora Veil wore off!"; - break; - case 'reflect': - actions += "" + side.getTeamName() + "'s Reflect wore off!"; - break; - case 'lightscreen': - actions += "" + side.getTeamName() + "'s Light Screen wore off!"; - break; - case 'safeguard': - actions += "" + side.getTeamName() + " is no longer protected by Safeguard!"; - break; - case 'mist': - actions += "" + side.getTeamName() + " is no longer protected by mist!"; - break; - case 'luckychant': - actions += "" + side.getTeamName() + "'s Lucky Chant wore off!"; - break; - case 'firepledge': - actions += "The sea of fire around " + side.getLowerTeamName() + " disappeared!"; - break; - case 'waterpledge': - actions += "The rainbow on " + side.getLowerTeamName() + "'s side disappeared!"; - break; - case 'grasspledge': - actions += "The swamp around " + side.getLowerTeamName() + " disappeared!"; - break; - default: - actions += "" + effect.name + " ended!"; - break; - } - break; - - } case '-weather': { - let effect = Tools.getEffect(args[1]); - let poke = this.getPokemon(kwargs.of) || undefined; - let ability = Tools.getEffect(kwargs.from); - this.changeWeather(effect.name, poke, !!kwargs.upkeep, ability); - break; - - } case '-fieldstart': { - let effect = Tools.getEffect(args[1]); - let poke = this.getPokemon(kwargs.of); - let fromeffect = Tools.getEffect(kwargs.from); - if (fromeffect && fromeffect.effectType === 'Ability') { - this.scene.abilityActivateAnim(poke!, fromeffect.name); - this.message('', "[" + poke!.getName(true) + "'s " + fromeffect.name + "!]"); - poke!.markAbility(fromeffect.name); - } - let maxTimeLeft = 0; - if (effect.id in {'electricterrain': 1, 'grassyterrain': 1, 'mistyterrain': 1, 'psychicterrain': 1}) { - for (let i = this.pseudoWeather.length - 1; i >= 0; i--) { - let pwName = this.pseudoWeather[i][0]; - if (pwName === 'Electric Terrain' || pwName === 'Grassy Terrain' || pwName === 'Misty Terrain' || pwName === 'Psychic Terrain') { - this.pseudoWeather.splice(i, 1); - continue; - } - } - if (this.gen > 6) maxTimeLeft = 8; - } - this.addPseudoWeather(effect.name, 5, maxTimeLeft); - - switch (effect.id) { - case 'wonderroom': - actions += "It created a bizarre area in which Defense and Sp. Def stats are swapped!"; - break; - case 'magicroom': - actions += "It created a bizarre area in which Pokémon's held items lose their effects!"; - break; - case 'gravity': - if (!this.fastForward) { - for (const side of this.sides) for (const active of side.active) { - if (active) { - this.scene.runOtherAnim('gravity' as ID, [active]); - } - } - } - actions += "Gravity intensified!"; - break; - case 'mudsport': - actions += "Electricity's power was weakened!"; - break; - case 'watersport': - actions += "Fire's power was weakened!"; - break; - case 'grassyterrain': - actions += "Grass grew to cover the battlefield!"; - break; - case 'mistyterrain': - actions += "Mist swirls around the battlefield!"; - break; - case 'electricterrain': - actions += "An electric current runs across the battlefield!"; - break; - case 'psychicterrain': - actions += "The battlefield got weird!"; - break; - case 'trickroom': - if (poke) { - actions += "" + poke.getName() + ' twisted the dimensions!'; - break; - } - // falls through - default: - actions += effect.name + " started!"; - break; - } - break; - - } case '-fieldend': { - let effect = Tools.getEffect(args[1]); - // let poke = this.getPokemon(kwargs.of); - this.removePseudoWeather(effect.name); - - switch (effect.id) { - case 'trickroom': - actions += 'The twisted dimensions returned to normal!'; - break; - case 'wonderroom': - actions += 'Wonder Room wore off, and Defense and Sp. Def stats returned to normal!'; - break; - case 'magicroom': - actions += "Magic Room wore off, and held items' effects returned to normal!"; - break; - case 'gravity': - actions += 'Gravity returned to normal!'; - break; - case 'mudsport': - actions += 'The effects of Mud Sport have faded.'; - break; - case 'watersport': - actions += 'The effects of Water Sport have faded.'; - break; - case 'grassyterrain': - actions += "The grass disappeared from the battlefield."; - break; - case 'mistyterrain': - actions += "The mist disappeared from the battlefield."; - break; - case 'electricterrain': - actions += "The electricity disappeared from the battlefield."; - break; - case 'psychicterrain': - actions += "The weirdness disappeared from the battlefield!"; - break; - default: - actions += effect.name + " ended!"; - break; - } - break; - - } case '-fieldactivate': { - let effect = Tools.getEffect(args[1]); - switch (effect.id) { - case 'perishsong': - actions += 'All Pokémon that heard the song will faint in three turns!'; - this.scene.updateStatbars(); - break; - case 'payday': - actions += 'Coins were scattered everywhere!'; - break; - case 'iondeluge': - actions += 'A deluge of ions showers the battlefield!'; - break; - default: - actions += '' + effect.name + ' hit!'; - break; - } - break; - - } case '-message': { - actions += BattleLog.escapeHTML(args[1]); - break; - - } case '-anim': { - let poke = this.getPokemon(args[1])!; - let move = Tools.getMove(args[2]); - if (this.checkActive(poke)) return; - let poke2 = this.getPokemon(args[3]); - this.scene.beforeMove(poke); - kwargs.silent = '.'; - this.useMove(poke, move, poke2, kwargs); - this.scene.afterMove(poke); - break; - - } case '-hint': { - this.message('', '(' + BattleLog.escapeHTML(args[1]) + ')'); - break; - - } default: { - if (this.errorCallback) this.errorCallback(this); - break; - }} - if (actions && actions.slice(-1) !== '>') actions += '
    '; + } + this.log(args, kwArgs); + break; } - if (actions) { - if (actions.slice(-6) === '
    ') actions = actions.slice(0, -6); - this.message('' + actions + '', ''); + case '-sidestart': { + let side = this.getSide(args[1]); + let effect = Tools.getEffect(args[2]); + side.addSideCondition(effect); + + switch (effect.id) { + case 'tailwind': + case 'auroraveil': + case 'reflect': + case 'lightscreen': + case 'safeguard': + case 'mist': + this.scene.updateWeather(); + break; + } + this.log(args, kwArgs); + break; } + case '-sideend': { + let side = this.getSide(args[1]); + let effect = Tools.getEffect(args[2]); + // let from = Tools.getEffect(kwArgs.from); + // let ofpoke = this.getPokemon(kwArgs.of); + side.removeSideCondition(effect.name); + this.log(args, kwArgs); + break; + } + case '-weather': { + let effect = Tools.getEffect(args[1]); + let poke = this.getPokemon(kwArgs.of) || undefined; + let ability = Tools.getEffect(kwArgs.from); + if (!effect.id || effect.id === 'none') { + kwArgs.from = this.weather; + } + this.changeWeather(effect.name, poke, !!kwArgs.upkeep, ability); + this.log(args, kwArgs); + break; + } + case '-fieldstart': { + let effect = Tools.getEffect(args[1]); + let poke = this.getPokemon(kwArgs.of); + let fromeffect = Tools.getEffect(kwArgs.from); + this.activateAbility(poke, fromeffect); + let maxTimeLeft = 0; + if (effect.id in {'electricterrain': 1, 'grassyterrain': 1, 'mistyterrain': 1, 'psychicterrain': 1}) { + for (let i = this.pseudoWeather.length - 1; i >= 0; i--) { + let pwName = this.pseudoWeather[i][0]; + if (pwName === 'Electric Terrain' || pwName === 'Grassy Terrain' || pwName === 'Misty Terrain' || pwName === 'Psychic Terrain') { + this.pseudoWeather.splice(i, 1); + continue; + } + } + if (this.gen > 6) maxTimeLeft = 8; + } + this.addPseudoWeather(effect.name, 5, maxTimeLeft); + + switch (effect.id) { + case 'gravity': + if (!this.fastForward) { + for (const side of this.sides) for (const active of side.active) { + if (active) { + this.scene.runOtherAnim('gravity' as ID, [active]); + } + } + } + break; + } + this.log(args, kwArgs); + break; + } + case '-fieldend': { + let effect = Tools.getEffect(args[1]); + // let poke = this.getPokemon(kwArgs.of); + this.removePseudoWeather(effect.name); + this.log(args, kwArgs); + break; + } + case '-fieldactivate': { + let effect = Tools.getEffect(args[1]); + switch (effect.id) { + case 'perishsong': + this.scene.updateStatbars(); + break; + } + this.log(args, kwArgs); + break; + } + case '-anim': { + let poke = this.getPokemon(args[1])!; + let move = Tools.getMove(args[2]); + if (this.checkActive(poke)) return; + let poke2 = this.getPokemon(args[3]); + this.scene.beforeMove(poke); + kwArgs.silent = '.'; + this.useMove(poke, move, poke2, kwArgs); + this.scene.afterMove(poke); + break; + } + case '-hint': case '-message': { + this.log(args, kwArgs); + break; + } + default: { + if (this.errorCallback) this.errorCallback(this); + break; + }} } /* parseSpriteData(name) { @@ -4350,34 +2905,35 @@ class Battle { this.preemptActivityQueue.push(command); this.add(command); } - runMajor(args: string[], kwargs: {[k: string]: string}, preempt?: boolean) { + runMajor(args: Args, kwArgs: KWArgs, preempt?: boolean) { switch (args[0]) { case 'start': { this.scene.teamPreviewEnd(); this.mySide.active[0] = null; this.yourSide.active[0] = null; - if (this.waitForResult()) return; this.start(); break; - } case 'upkeep': { + } + case 'upkeep': { this.usesUpkeep = true; this.updatePseudoWeatherLeft(); this.updateToxicTurns(); break; - } case 'turn': { - if (this.endPrevAction()) return; + } + case 'turn': { this.setTurn(args[1]); + this.log(args); break; - } case 'tier': { - if (!args[1]) args[1] = ''; - for (let i in kwargs) args[1] += '[' + i + '] ' + kwargs[i]; - this.scene.log('
    Format:
    ' + BattleLog.escapeHTML(args[1]) + '
    '); + } + case 'tier': { this.tier = args[1]; if (this.tier.slice(-13) === 'Random Battle') { this.speciesClause = true; } + this.log(args); break; - } case 'gametype': { + } + case 'gametype': { this.gameType = args[1] as any; switch (args[1]) { default: @@ -4396,60 +2952,19 @@ class Battle { } this.scene.updateGen(); break; - } case 'variation': { - this.scene.log('
    Variation: ' + BattleLog.escapeHTML(args[1]) + '
    '); + } + case 'rule': { + let ruleName = args[1].split(': ')[0]; + if (ruleName === 'Species Clause') this.speciesClause = true; + this.log(args); break; - } case 'rule': { - let ruleArgs = args[1].split(': '); - this.scene.log('
    ' + BattleLog.escapeHTML(ruleArgs[0]) + (ruleArgs[1] ? ':' : '') + ' ' + BattleLog.escapeHTML(ruleArgs[1] || '') + '
    '); - if (ruleArgs[0] === 'Species Clause') this.speciesClause = true; - break; - } case 'rated': { + } + case 'rated': { this.rated = true; - this.scene.log('
    ' + (BattleLog.escapeHTML(args[1]) || 'Rated battle') + '
    '); + this.log(args); break; - } case ':': { - break; - } case 'chat': case 'c': case 'c:': { - let pipeIndex = args[1].indexOf('|'); - if (args[0] === 'c:') { - args[1] = args[1].slice(pipeIndex + 1); - pipeIndex = args[1].indexOf('|'); - } - let name = args[1].slice(0, pipeIndex); - let rank = name.charAt(0); - if (this.ignoreSpects && (rank === ' ' || rank === '+')) break; - if (this.ignoreOpponent && (rank === '\u2605' || rank === '\u2606') && toUserid(name) !== app.user.get('userid')) break; - if (window.app && app.ignore && app.ignore[toUserid(name)] && (rank === ' ' || rank === '+' || rank === '\u2605' || rank === '\u2606')) break; - let message = args[1].slice(pipeIndex + 1); - let isHighlighted = window.app && app.rooms && app.rooms[this.roomid].getHighlight(message); - let parsedMessage = BattleLog.parseChatMessage(message, name, '', isHighlighted); - if (!Array.isArray(parsedMessage)) parsedMessage = [parsedMessage]; - for (let i = 0; i < parsedMessage.length; i++) { - if (!parsedMessage[i]) continue; - this.scene.log(parsedMessage[i], preempt); - } - if (isHighlighted) { - let notifyTitle = "Mentioned by " + name + " in " + this.roomid; - app.rooms[this.roomid].notifyOnce(notifyTitle, "\"" + message + "\"", 'highlight'); - } - break; - } case 'chatmsg': { - this.scene.log('
    ' + BattleLog.escapeHTML(args[1]) + '
    ', preempt); - break; - } case 'chatmsg-raw': case 'raw': case 'html': { - this.scene.log('
    ' + BattleLog.sanitizeHTML(args[1]) + '
    ', preempt); - break; - } case 'error': { - this.scene.log('
    ' + BattleLog.escapeHTML(args[1]) + '
    ', preempt); - break; - } case 'pm': { - this.scene.log('
    ' + BattleLog.escapeHTML(args[1]) + ': (Private to ' + BattleLog.escapeHTML(args[3]) + ') ' + BattleLog.parseMessage(args[4]) + ''); - break; - } case 'askreg': { - this.scene.log('
    Register an account to protect your ladder rating!
    '); - break; - } case 'inactive': { + } + case 'inactive': { if (!this.kickingInactive) this.kickingInactive = true; if (args[1].slice(0, 11) === "Time left: ") { this.kickingInactive = parseInt(args[1].slice(11), 10) || true; @@ -4469,15 +2984,15 @@ class Battle { this.kickingInactive = parseInt(args[1].slice(hasIndex + 5), 10) || true; } } - this.scene.log('
    ' + BattleLog.escapeHTML(args[1]) + '
    ', preempt); + this.log(args, undefined, preempt); break; - } case 'inactiveoff': { + } + case 'inactiveoff': { this.kickingInactive = false; - this.scene.log('
    ' + BattleLog.escapeHTML(args[1]) + '
    ', preempt); + this.log(args, undefined, preempt); break; - } case 'timer': { - break; - } case 'join': case 'j': { + } + case 'join': case 'j': { if (this.roomid) { let room = app.rooms[this.roomid]; let user = args[1]; @@ -4490,10 +3005,11 @@ class Battle { room.userList.updateNoUsersOnline(); } if (!this.ignoreSpects) { - this.scene.log('
    ' + BattleLog.escapeHTML(args[1]) + ' joined.
    ', preempt); + this.log(args, undefined, preempt); } break; - } case 'leave': case 'l': { + } + case 'leave': case 'l': { if (this.roomid) { let room = app.rooms[this.roomid]; let user = args[1]; @@ -4505,85 +3021,52 @@ class Battle { room.userList.updateNoUsersOnline(); } if (!this.ignoreSpects) { - this.scene.log('
    ' + BattleLog.escapeHTML(args[1]) + ' left.
    ', preempt); + this.log(args, undefined, preempt); } break; - } case 'J': case 'L': case 'N': case 'n': case 'spectator': case 'spectatorleave': { - break; - } case 'player': { + } + case 'player': { let side = this.getSide(args[1]); side.setName(args[2]); if (args[3]) side.setAvatar(args[3]); this.scene.updateSidebar(side); if (this.joinButtons) this.scene.hideJoinButtons(); + this.log(args); break; - } case 'teamsize': { + } + case 'teamsize': { let side = this.getSide(args[1]); side.totalPokemon = parseInt(args[2], 10); this.scene.updateSidebar(side); break; - } case 'win': { - this.winner(args[1]); + } + case 'win': case 'tie': { + this.winner(args[0] === 'tie' ? undefined : args[1]); break; - } case 'tie': { - this.winner(); - break; - } case 'prematureend': { + } + case 'prematureend': { this.prematureEnd(); break; - } case 'clearpoke': { + } + case 'clearpoke': { this.p1.clearPokemon(); this.p2.clearPokemon(); break; - } case 'poke': { + } + case 'poke': { let pokemon = this.getPokemon('new: ' + args[1], args[2])!; if (args[3] === 'item') { pokemon.item = '(exists)'; } break; - } case 'detailschange': { - let poke = this.getPokemon(args[1])!; - poke.removeVolatile('formechange' as ID); - poke.removeVolatile('typeadd' as ID); - poke.removeVolatile('typechange' as ID); - - let newSpecies = args[2]; - let commaIndex = newSpecies.indexOf(','); - if (commaIndex !== -1) { - let level = newSpecies.substr(commaIndex + 1).trim(); - if (level.charAt(0) === 'L') { - poke.level = parseInt(level.substr(1), 10); - } - newSpecies = args[2].substr(0, commaIndex); - } - let template = Tools.getTemplate(newSpecies); - - poke.species = newSpecies; - poke.ability = poke.baseAbility = (template.abilities ? template.abilities['0'] : ''); - poke.weightkg = template.weightkg; - - poke.details = args[2]; - poke.searchid = args[1].substr(0, 2) + args[1].substr(3) + '|' + args[2]; - - this.scene.animTransform(poke, true, true); - if (toId(newSpecies) === 'greninjaash') { - this.message('' + poke.getName() + ' became Ash-Greninja!'); - } else if (toId(newSpecies) === 'mimikyubusted') { - this.message('' + poke.getName() + "'s disguise was busted!"); - } else if (toId(newSpecies) === 'zygardecomplete') { - this.message('' + poke.getName() + ' transformed into its Complete Forme!'); - } else if (toId(newSpecies) === 'necrozmaultra') { - this.message('' + poke.getName() + ' regained its true power through Ultra Burst!'); - } - break; - } case 'teampreview': { + } + case 'teampreview': { this.teamPreviewCount = parseInt(args[1], 10); this.scene.teamPreview(); break; - } case 'switch': case 'drag': case 'replace': { + } + case 'switch': case 'drag': case 'replace': { this.endLastTurn(); - if (!this.hasPreMoveMessage && this.waitForResult()) return; - this.hasPreMoveMessage = false; let poke = this.getPokemon('switchin: ' + args[1], args[2])!; let slot = poke.slot; poke.healthParse(args[3]); @@ -4598,90 +3081,113 @@ class Battle { } else { poke.side.dragIn(poke); } + this.log(args, kwArgs); break; - } case 'faint': { - if (this.waitForResult()) return; + } + case 'faint': { let poke = this.getPokemon(args[1])!; poke.side.faint(poke); + this.log(args, kwArgs); break; - } case 'swap': { + } + case 'swap': { if (isNaN(Number(args[2]))) { let poke = this.getPokemon(args[1])!; - poke.side.swapWith(poke, this.getPokemon(args[2])!, kwargs); + poke.side.swapWith(poke, this.getPokemon(args[2])!, kwArgs); } else { let poke = this.getPokemon(args[1])!; - poke.side.swapTo(poke, parseInt(args[2], 10), kwargs); + poke.side.swapTo(poke, parseInt(args[2], 10), kwArgs); } + this.log(args, kwArgs); break; - } case 'move': { + } + case 'move': { this.endLastTurn(); - if ((!kwargs.from || kwargs.from === 'lockedmove') && !this.hasPreMoveMessage && this.waitForResult()) return; - this.hasPreMoveMessage = false; this.resetTurnsSinceMoved(); let poke = this.getPokemon(args[1])!; let move = Tools.getMove(args[2]); if (this.checkActive(poke)) return; let poke2 = this.getPokemon(args[3]); this.scene.beforeMove(poke); - this.useMove(poke, move, poke2, kwargs); + this.useMove(poke, move, poke2, kwArgs); + this.log(args, kwArgs); this.scene.afterMove(poke); break; - } case 'cant': { + } + case 'cant': { this.endLastTurn(); this.resetTurnsSinceMoved(); - if (!this.hasPreMoveMessage && this.waitForResult()) return; - this.hasPreMoveMessage = false; let poke = this.getPokemon(args[1])!; let effect = Tools.getEffect(args[2]); let move = Tools.getMove(args[3]); - this.cantUseMove(poke, effect, move, kwargs); + this.cantUseMove(poke, effect, move, kwArgs); break; - } case 'message': { - this.message(BattleLog.escapeHTML(args[1])); - break; - } case 'bigerror': { - this.message('
    ' + BattleLog.escapeHTML(args[1]).replace(/\|/g, '
    ') + '
    '); - break; - } case 'done': case '': { - if (this.ended || this.endPrevAction()) return; - break; - } case 'warning': { - this.message('Warning: ' + BattleLog.escapeHTML(args[1])); - this.message('Bug? Report it to the replay viewer\'s Smogon thread'); - this.scene.wait(1000); - break; - } case 'gen': { + } + case 'gen': { this.gen = parseInt(args[1], 10); this.scene.updateGen(); + this.log(args); break; - } case 'callback': { - args.shift(); - if (this.customCallback) this.customCallback(this, args[0], args, kwargs); + } + case 'callback': { + if (this.customCallback) this.customCallback(this, args[1], args.slice(1), kwArgs); break; - } case 'debug': { - args.shift(); - const name = args.join(' '); - this.scene.log('
    [DEBUG] ' + BattleLog.escapeHTML(name) + '.
    ', preempt); - break; - } case 'seed': case 'choice': { - break; - } case 'unlink': { - let user = toId(args[2]) || toId(args[1]); - this.scene.unlink(user, !!args[2]); - break; - } case 'fieldhtml': { + } + case 'fieldhtml': { this.playbackState = Playback.Seeking; // force seeking to prevent controls etc this.scene.setFrameHTML(BattleLog.sanitizeHTML(args[1])); break; - } case 'controlshtml': { + } + case 'controlshtml': { this.scene.setControlsHTML(BattleLog.sanitizeHTML(args[1])); break; - } default: { - this.scene.log('
    Unknown command: ' + BattleLog.escapeHTML(args[0]) + '
    '); - if (this.errorCallback) this.errorCallback(this); + } + default: { + this.log(args, kwArgs); break; }} } + static lineParse(str: string): {args: Args, kwArgs: KWArgs} { + if (!str.startsWith('|')) { + return {args: ['', str], kwArgs: {}}; + } + if (str === '|') { + return {args: ['done'], kwArgs: {}}; + } + const index = str.indexOf('|', 1); + const cmd = str.slice(1, index); + switch (cmd) { + case 'chatmsg': case 'chatmsg-raw': case 'raw': case 'error': case 'html': + case 'inactive': case 'inactiveoff': case 'warning': + case 'fieldhtml': case 'controlshtml': case 'bigerror': + case 'debug': case 'tier': + return {args: [cmd, str.slice(index + 1)], kwArgs: {}}; + case 'c': case 'chat': + // three parts + const index2a = str.indexOf('|', index + 1); + return {args: [cmd, str.slice(index + 1, index2a), str.slice(index2a + 1)], kwArgs: {}}; + case 'c:': + // four parts + const index2b = str.indexOf('|', index + 1); + const index3b = str.indexOf('|', index2b + 1); + return {args: + [cmd, str.slice(index + 1, index2b), str.slice(index2b + 1, index3b), str.slice(index3b + 1)], + kwArgs: {}}; + } + let args = str.slice(1).split('|') as [string, ...string[]]; + let kwArgs = {} as {[k: string]: string}; + while (args.length) { + const lastArg = args[args.length - 1]; + if (lastArg.charAt(0) !== '[') break; + const bracketPos = lastArg.indexOf(']'); + if (bracketPos <= 0) break; + // default to '.' so it evaluates to boolean true + kwArgs[lastArg.slice(1, bracketPos)] = lastArg.slice(bracketPos + 1).trim() || '.'; + args.pop(); + } + return {args, kwArgs}; + } + run(str: string, preempt?: boolean) { if (this.preemptActivityQueue.length && str === this.preemptActivityQueue[0]) { this.preemptActivityQueue.shift(); @@ -4689,78 +3195,45 @@ class Battle { return; } if (!str) return; - if (str.charAt(0) !== '|' || str.substr(0, 2) === '||') { - if (str.charAt(0) === '|') str = str.substr(2); - this.scene.log('
    ' + BattleLog.escapeHTML(str) + '
    ', preempt); + const {args, kwArgs} = Battle.lineParse(str); + + if (this.scene.maybeCloseMessagebar(args, kwArgs)) { + this.activityStep--; + this.activeMoveIsSpread = null; return; } - let args = ['done']; - let kwargs = {} as {[k: string]: string}; - if (str !== '|') { - args = str.substr(1).split('|'); - } - switch (args[0]) { - case 'c': case 'c:': case 'chat': - case 'chatmsg': case 'chatmsg-raw': case 'raw': case 'error': case 'html': - case 'inactive': case 'inactiveoff': case 'warning': - case 'fieldhtml': case 'controlshtml': case 'bigerror': - // chat is preserved untouched - args = [args[0], str.slice(args[0].length + 2)]; - break; - default: - // parse kwargs - while (args.length) { - let argstr = args[args.length - 1]; - if (argstr.substr(0, 1) !== '[') break; - let bracketPos = argstr.indexOf(']'); - if (bracketPos <= 0) break; - // default to '.' so it evaluates to boolean true - kwargs[argstr.substr(1, bracketPos - 1)] = (argstr.substr(bracketPos + 1).trim() || '.'); - args.pop(); - } - } // parse the next line if it's a minor: runMinor needs it parsed to determine when to merge minors - let nextLine = ''; - let nextArgs = ['']; - let nextKwargs = {} as {[k: string]: string}; - nextLine = this.activityQueue[this.activityStep + 1] || ''; + let nextArgs = [''] as Args; + let nextKwargs = {} as KWArgs; + const nextLine = this.activityQueue[this.activityStep + 1] || ''; if (nextLine && nextLine.substr(0, 2) === '|-') { - nextLine = nextLine.substr(1).trim(); - nextArgs = nextLine.split('|'); - while (nextArgs[nextArgs.length - 1] && nextArgs[nextArgs.length - 1].substr(0, 1) === '[') { - let bracketPos = nextArgs[nextArgs.length - 1].indexOf(']'); - if (bracketPos <= 0) break; - let argstr = nextArgs.pop()!; - // default to '.' so it evaluates to boolean true - nextKwargs[argstr.substr(1, bracketPos - 1)] = (argstr.substr(bracketPos + 1).trim() || '.'); - } + ({args: nextArgs, kwArgs: nextKwargs} = Battle.lineParse(nextLine)); } if (this.debug) { - if (args[0].substr(0, 1) === '-') { - this.runMinor(args, kwargs, preempt, nextArgs, nextKwargs); + if (args[0].charAt(0) === '-' || args[0] === 'detailschange') { + this.runMinor(args, kwArgs, nextArgs, nextKwargs); } else { - this.runMajor(args, kwargs, preempt); + this.runMajor(args, kwArgs, preempt); } } else { try { - if (args[0].substr(0, 1) === '-') { - this.runMinor(args, kwargs, preempt, nextArgs, nextKwargs); + if (args[0].charAt(0) === '-' || args[0] === 'detailschange') { + this.runMinor(args, kwArgs, nextArgs, nextKwargs); } else { - this.runMajor(args, kwargs, preempt); + this.runMajor(args, kwArgs, preempt); } } catch (e) { - this.scene.log('
    Error parsing: ' + BattleLog.escapeHTML(str) + ' (' + BattleLog.escapeHTML('' + e) + ')
    ', preempt); + this.log(['majorerror', 'Error parsing: ' + str + ' (' + e + ')']); if (e.stack) { - let stack = BattleLog.escapeHTML('' + e.stack).split('\n'); - for (let i = 0; i < stack.length; i++) { - if (/\brun\b/.test(stack[i])) { - stack.length = i; + let stack = ('' + e.stack).split('\n'); + for (const line of stack) { + if (/\brun\b/.test(line)) { break; } + this.log(['error', line]); } - this.scene.log('
    ' + stack.join('
    ') + '
    ', preempt); } if (this.errorCallback) this.errorCallback(this); } @@ -4773,22 +3246,6 @@ class Battle { } } } - endPrevAction() { - this.hasPreMoveMessage = false; - if (this.minorQueue.length) { - this.runMinor(); - this.activityStep--; - return true; - } - if (this.resultWaiting || this.scene.messagebarOpen) { - this.scene.closeMessagebar(); - this.activityStep--; - this.resultWaiting = false; - this.activeMoveIsSpread = null; - return true; - } - return false; - } checkActive(poke: Pokemon) { if (!poke.side.active[poke.slot]) { // SOMEONE jumped in in the middle of a replay. <_< @@ -4796,11 +3253,6 @@ class Battle { } return false; } - waitForResult() { - if (this.endPrevAction()) return true; - this.resultWaiting = true; - return false; - } pause() { this.paused = true; @@ -4851,6 +3303,7 @@ class Battle { this.scene.startAnimations(); let animations; while (!animations) { + this.waitForAnimations = true; if (this.activityStep >= this.activityQueue.length) { this.fastForwardOff(); if (this.ended) { @@ -4864,7 +3317,11 @@ class Battle { if (this.paused && !this.fastForward) return; this.run(this.activityQueue[this.activityStep]); this.activityStep++; - animations = this.scene.finishAnimations(); + if (this.waitForAnimations === true) { + animations = this.scene.finishAnimations(); + } else if (this.waitForAnimations === 'simult') { + this.scene.timeOffset = 0; + } } if (this.playbackState === Playback.Paused) return;