+ * @license MIT
+ */
+
+class ModifiableValue {
+ value = 0;
+ maxValue = 0;
+ comment: string[];
+ battle: Battle;
+ pokemon: Pokemon | null;
+ serverPokemon: ServerPokemon;
+ itemName: string;
+ abilityName: string;
+ weatherName: string;
+ isAccuracy = false;
+ constructor(battle: Battle, pokemon: Pokemon | null, serverPokemon: ServerPokemon) {
+ this.comment = [];
+ this.battle = battle;
+ this.pokemon = pokemon;
+ this.serverPokemon = serverPokemon;
+
+ this.itemName = Dex.getItem(serverPokemon.item).name;
+ this.abilityName = Dex.getAbility(serverPokemon.ability || (pokemon && pokemon.ability) || serverPokemon.baseAbility).name;
+ this.weatherName = Dex.getMove(battle.weather).name;
+ }
+ reset(value = 0, isAccuracy?: boolean) {
+ this.value = value;
+ this.maxValue = 0;
+ this.isAccuracy = !!isAccuracy;
+ this.comment = [];
+ }
+ tryItem(itemName: string) {
+ if (itemName !== this.itemName) return false;
+ if (this.battle.hasPseudoWeather('Magic Room')) {
+ this.comment.push(` (${itemName} suppressed by Magic Room)`);
+ return false;
+ }
+ if (this.pokemon && this.pokemon.volatiles['embargo']) {
+ this.comment.push(` (${itemName} suppressed by Embargo)`);
+ return false;
+ }
+ const ignoreKlutz = ["Macho Brace", "Power Anklet", "Power Band", "Power Belt", "Power Bracer", "Power Lens", "Power Weight"];
+ if (this.tryAbility('Klutz') && !ignoreKlutz.includes(itemName)) {
+ this.comment.push(` (${itemName} suppressed by Klutz)`);
+ return false;
+ }
+ return true;
+ }
+ tryAbility(abilityName: string) {
+ if (abilityName !== this.abilityName) return false;
+ if (this.pokemon && this.pokemon.volatiles['gastroacid']) {
+ this.comment.push(` (${abilityName} suppressed by Gastro Acid)`);
+ return false;
+ }
+ return true;
+ }
+ tryWeather(weatherName?: string) {
+ if (!this.weatherName) return false;
+ if (!weatherName) weatherName = this.weatherName;
+ else if (weatherName !== this.weatherName) return false;
+ for (const side of this.battle.sides) {
+ for (const active of side.active) {
+ if (active && ['Air Lock', 'Cloud Nine'].includes(active.ability)) {
+ this.comment.push(` (${weatherName} suppressed by ${active.ability})`);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ itemModify(factor: number, itemName?: string) {
+ if (!itemName) itemName = this.itemName;
+ if (!itemName) return false;
+ if (!this.tryItem(itemName)) return false;
+ return this.modify(factor, itemName);
+ }
+ abilityModify(factor: number, abilityName: string) {
+ if (!this.tryAbility(abilityName)) return false;
+ return this.modify(factor, abilityName);
+ }
+ weatherModify(factor: number, weatherName?: string, name?: string) {
+ if (!weatherName) weatherName = this.weatherName;
+ if (!weatherName) return false;
+ if (!this.tryWeather(weatherName)) return false;
+ return this.modify(factor, name || weatherName);
+ }
+ modify(factor: number, name?: string) {
+ if (factor === 0) {
+ if (name) this.comment.push(` (${name})`);
+ this.value = 0;
+ this.maxValue = 0;
+ return true;
+ }
+ if (name) this.comment.push(` (${factor}× from ${name})`);
+ this.value *= factor;
+ if (!(name === 'Technician' && this.maxValue > 60)) this.maxValue *= factor;
+ return true;
+ }
+ set(value: number, reason?: string) {
+ if (reason) this.comment.push(` (${reason})`);
+ this.value = value;
+ this.maxValue = 0;
+ return true;
+ }
+ setRange(value: number, maxValue: number, reason?: string) {
+ if (reason) this.comment.push(` (${reason})`);
+ this.value = value;
+ this.maxValue = maxValue;
+ return true;
+ }
+ toString() {
+ let valueString;
+ if (this.isAccuracy) {
+ valueString = this.value ? `${this.value}%` : `can't miss`;
+ } else {
+ valueString = this.value ? `${this.value}` : ``;
+ }
+ if (this.maxValue) {
+ valueString += ` to ${this.maxValue}` + (this.isAccuracy ? '%' : '');
+ }
+ return valueString + this.comment.join('');
+ }
+}
+
+class BattleTooltips {
+ battle: Battle;
+
+ constructor(battle: Battle) {
+ this.battle = battle;
+ }
+
+ // tooltips
+ // Touch delay, pressing finger more than that time will cause the tooltip to open.
+ // Shorter time will cause the button to click
+ static LONG_TAP_DELAY = 350; // ms
+ static longTapTimeout = 0;
+ static elem: HTMLDivElement | null = null;
+ static parentElem: HTMLElement | null = null;
+ static isLocked = false;
+
+ static hideTooltip() {
+ if (!BattleTooltips.elem) return;
+ BattleTooltips.cancelLongTap();
+ BattleTooltips.elem.parentNode!.removeChild(BattleTooltips.elem);
+ BattleTooltips.elem = null;
+ BattleTooltips.parentElem = null;
+ BattleTooltips.isLocked = false;
+ $('#tooltipwrapper').removeClass('tooltip-locked');
+ }
+
+ static cancelLongTap() {
+ if (BattleTooltips.longTapTimeout) {
+ clearTimeout(BattleTooltips.longTapTimeout);
+ BattleTooltips.longTapTimeout = 0;
+ }
+ }
+
+ lockTooltip() {
+ if (BattleTooltips.elem && !BattleTooltips.isLocked) {
+ BattleTooltips.isLocked = true;
+ $('#tooltipwrapper').addClass('tooltip-locked');
+ }
+ }
+
+ handleTouchEnd(e: TouchEvent) {
+ BattleTooltips.cancelLongTap();
+
+ if (!BattleTooltips.isLocked) BattleTooltips.hideTooltip();
+ }
+
+ listen(elem: HTMLElement) {
+ const $elem = $(elem);
+ $elem.on('mouseover', '.has-tooltip', this.showTooltipEvent);
+ $elem.on('click', '.has-tooltip', this.clickTooltipEvent);
+ $elem.on('touchstart', '.has-tooltip', this.holdLockTooltipEvent);
+ $elem.on('touchend', '.has-tooltip', BattleTooltips.unshowTooltip);
+ $elem.on('touchleave', '.has-tooltip', BattleTooltips.unshowTooltip);
+ $elem.on('touchcancel', '.has-tooltip', BattleTooltips.unshowTooltip);
+ $elem.on('focus', '.has-tooltip', this.showTooltipEvent);
+ $elem.on('mouseout', '.has-tooltip', BattleTooltips.unshowTooltip);
+ $elem.on('mousedown', '.has-tooltip', this.holdLockTooltipEvent);
+ $elem.on('blur', '.has-tooltip', BattleTooltips.unshowTooltip);
+ $elem.on('mouseup', '.has-tooltip', BattleTooltips.unshowTooltip);
+ }
+
+ clickTooltipEvent = (e: Event) => {
+ if (BattleTooltips.isLocked) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ }
+ };
+ /**
+ * An event that will lock a tooltip if held down
+ *
+ * (Namely, a long-tap or long-click)
+ */
+ holdLockTooltipEvent = (e: Event) => {
+ if (BattleTooltips.isLocked) BattleTooltips.hideTooltip();
+ const target = e.currentTarget as HTMLElement;
+ this.showTooltip(target);
+ let factor = (e.type === 'mousedown' && target.tagName === 'BUTTON' ? 2 : 1);
+
+ BattleTooltips.longTapTimeout = setTimeout(() => {
+ BattleTooltips.longTapTimeout = 0;
+ this.lockTooltip();
+ }, BattleTooltips.LONG_TAP_DELAY * factor);
+ };
+
+ showTooltipEvent = (e: Event) => {
+ if (BattleTooltips.isLocked) return;
+ this.showTooltip(e.currentTarget as HTMLElement);
+ };
+
+ /**
+ * Only hides tooltips if they're not locked
+ */
+ static unshowTooltip() {
+ if (BattleTooltips.isLocked) return;
+ BattleTooltips.hideTooltip();
+ }
+
+ showTooltip(elem: HTMLElement) {
+ const args = (elem.dataset.tooltip || '').split('|');
+ const [type] = args;
+ /**
+ * If false, we instead attach the tooltip above the parent element.
+ * This is important for the move/switch menus so the tooltip doesn't
+ * cover up buttons above the hovered button.
+ */
+ const ownHeight = !!elem.dataset.ownheight;
+
+ let buf: string;
+ switch (type) {
+ case 'move':
+ case 'zmove': { // move|MOVE|ACTIVEPOKEMON
+ let move = this.battle.dex.getMove(args[1]);
+ let index = parseInt(args[2], 10);
+ let pokemon = this.battle.mySide.active[index]!;
+ let serverPokemon = this.battle.myPokemon![index];
+ buf = this.showMoveTooltip(move, type === 'zmove', pokemon, serverPokemon);
+ break;
+ }
+
+ case 'pokemon': { // pokemon|SIDE|POKEMON
+ // mouse over sidebar pokemon
+ // pokemon definitely exists, serverPokemon always ignored
+ let sideIndex = parseInt(args[1], 10);
+ let side = this.battle.sides[sideIndex];
+ let pokemon = side.pokemon[parseInt(args[2], 10)];
+ buf = this.showPokemonTooltip(pokemon);
+ break;
+ }
+ case 'activepokemon': { // activepokemon|SIDE|ACTIVE
+ // mouse over active pokemon
+ // pokemon definitely exists, serverPokemon maybe
+ let sideIndex = parseInt(args[1], 10);
+ let side = this.battle.sides[sideIndex];
+ let activeIndex = parseInt(args[2], 10);
+ let pokemon = side.active[activeIndex];
+ let serverPokemon = null;
+ if (sideIndex === 0 && this.battle.myPokemon) {
+ serverPokemon = this.battle.myPokemon[activeIndex];
+ }
+ if (!pokemon) return false;
+ buf = this.showPokemonTooltip(pokemon, serverPokemon, true);
+ break;
+ }
+ case 'switchpokemon': { // switchpokemon|POKEMON
+ // mouse over switchable pokemon
+ // serverPokemon definitely exists, sidePokemon maybe
+ let side = this.battle.sides[0];
+ let activeIndex = parseInt(args[1], 10);
+ let pokemon = null;
+ if (activeIndex < side.active.length) {
+ pokemon = side.active[activeIndex];
+ }
+ let serverPokemon = this.battle.myPokemon![activeIndex];
+ buf = this.showPokemonTooltip(pokemon, serverPokemon);
+ break;
+ }
+ default:
+ throw new Error(`unrecognized type`);
+ }
+
+ let offset = {
+ left: 150,
+ top: 500,
+ };
+ if (elem) offset = $(elem).offset()!;
+ let x = offset.left - 2;
+ if (elem) {
+ offset = (ownHeight ? $(elem) : $(elem).parent()).offset()!;
+ }
+ let y = offset.top - 5;
+
+ if (y < 140) y = 140;
+ // if (x > room.leftWidth + 335) x = room.leftWidth + 335;
+ if (x > $(window).width()! - 305) x = Math.max($(window).width()! - 305, 0);
+ if (x < 0) x = 0;
+
+ let $wrapper = $('#tooltipwrapper');
+ if (!$wrapper.length) {
+ $wrapper = $(``);
+ $(document.body).append($wrapper);
+ } else {
+ $wrapper.removeClass('tooltip-locked');
+ }
+ $wrapper.css({
+ left: x,
+ top: y,
+ });
+ buf = ``;
+ $wrapper.html(buf).appendTo(document.body);
+ BattleTooltips.elem = $wrapper.find('.tooltip')[0] as HTMLDivElement;
+ BattleTooltips.isLocked = false;
+ if (elem) {
+ let height = $(BattleTooltips.elem).height()!;
+ if (height > y) {
+ y += height + 10;
+ if (ownHeight) y += $(elem).height()!;
+ else y += $(elem).parent().height()!;
+ $wrapper.css('top', y);
+ }
+ }
+ BattleTooltips.parentElem = elem;
+ return true;
+ }
+
+ hideTooltip() {
+ BattleTooltips.hideTooltip();
+ }
+
+ static zMoveEffects: {[zEffect: string]: string} = {
+ 'clearnegativeboost': "Restores negative stat stages to 0",
+ 'crit2': "Crit ratio +2",
+ 'heal': "Restores HP 100%",
+ 'curse': "Restores HP 100% if user is Ghost type, otherwise Attack +1",
+ 'redirect': "Redirects opposing attacks to user",
+ 'healreplacement': "Restores replacement's HP 100%",
+ };
+
+ getStatusZMoveEffect(move: Move) {
+ if (move.zMoveEffect in BattleTooltips.zMoveEffects) {
+ return BattleTooltips.zMoveEffects[move.zMoveEffect];
+ }
+ let boostText = '';
+ if (move.zMoveBoost) {
+ let boosts = Object.keys(move.zMoveBoost) as StatName[];
+ boostText = boosts.map(stat =>
+ BattleStats[stat] + ' +' + move.zMoveBoost![stat]
+ ).join(', ');
+ }
+ return boostText;
+ }
+
+ static zMoveTable: {[type in TypeName]: string} = {
+ Poison: "Acid Downpour",
+ Fighting: "All-Out Pummeling",
+ Dark: "Black Hole Eclipse",
+ Grass: "Bloom Doom",
+ Normal: "Breakneck Blitz",
+ Rock: "Continental Crush",
+ Steel: "Corkscrew Crash",
+ Dragon: "Devastating Drake",
+ Electric: "Gigavolt Havoc",
+ Water: "Hydro Vortex",
+ Fire: "Inferno Overdrive",
+ Ghost: "Never-Ending Nightmare",
+ Bug: "Savage Spin-Out",
+ Psychic: "Shattered Psyche",
+ Ice: "Subzero Slammer",
+ Flying: "Supersonic Skystrike",
+ Ground: "Tectonic Rage",
+ Fairy: "Twinkle Tackle",
+ "???": "",
+ };
+
+ showMoveTooltip(move: Move, isZ: boolean, pokemon: Pokemon, serverPokemon: ServerPokemon) {
+ let text = '';
+
+ let zEffect = '';
+ let foeActive = pokemon.side.foe.active;
+ // TODO: move this somewhere it makes more sense
+ if (pokemon.ability === '(suppressed)') serverPokemon.ability = '(suppressed)';
+ let ability = toId(serverPokemon.ability || pokemon.ability || serverPokemon.baseAbility);
+
+ let value = new ModifiableValue(this.battle, pokemon, serverPokemon);
+
+ if (isZ) {
+ let item = this.battle.dex.getItem(serverPokemon.item);
+ if (item.zMoveFrom === move.name) {
+ move = this.battle.dex.getMove(item.zMove as string);
+ } else if (move.category === 'Status') {
+ move = new Move(move.id, "", {
+ ...move,
+ name: 'Z-' + move.name,
+ });
+ zEffect = this.getStatusZMoveEffect(move);
+ } else {
+ const zMove = this.battle.dex.getMove(BattleTooltips.zMoveTable[item.zMoveType as TypeName]);
+ move = new Move(zMove.id, zMove.name, {
+ ...zMove,
+ category: move.category,
+ basePower: move.zMovePower,
+ });
+ // TODO: Weather Ball type-changing shenanigans
+ }
+ }
+
+ text += '' + move.name + '
';
+
+ // Handle move type for moves that vary their type.
+ let [moveType, category] = this.getMoveType(move, value);
+
+ text += Dex.getTypeIcon(moveType);
+ text += ` 
`;
+
+ // Check if there are more than one active Pokémon to check for multiple possible BPs.
+ let showingMultipleBasePowers = false;
+ if (category !== 'Status' && foeActive.length > 1) {
+ // We check if there is a difference in base powers to note it.
+ // Otherwise, it is just shown as in singles.
+ // The trick is that we need to calculate it first for each Pokémon to see if it changes.
+ let prevBasePower: string | null = null;
+ let basePower: string = '';
+ let difference = false;
+ let basePowers = [];
+ for (const active of foeActive) {
+ if (!active) continue;
+ value = this.getMoveBasePower(move, moveType, value, active);
+ basePower = '' + value;
+ if (prevBasePower === null) prevBasePower = basePower;
+ if (prevBasePower !== basePower) difference = true;
+ basePowers.push('Base power vs ' + active.name + ': ' + basePower);
+ }
+ if (difference) {
+ text += '' + basePowers.join('
') + '
';
+ showingMultipleBasePowers = true;
+ }
+ // Falls through to not to repeat code on showing the base power.
+ }
+ if (!showingMultipleBasePowers && category !== 'Status') {
+ let activeTarget = foeActive[0] || foeActive[1] || foeActive[2];
+ value = this.getMoveBasePower(move, moveType, value, activeTarget);
+ text += 'Base power: ' + value + '
';
+ }
+
+ let accuracy = this.getMoveAccuracy(move, value);
+
+ // Deal with Nature Power special case, indicating which move it calls.
+ if (move.id === 'naturepower') {
+ let calls;
+ if (this.battle.gen > 5) {
+ if (this.battle.hasPseudoWeather('Electric Terrain')) {
+ calls = 'Thunderbolt';
+ } else if (this.battle.hasPseudoWeather('Grassy Terrain')) {
+ calls = 'Energy Ball';
+ } else if (this.battle.hasPseudoWeather('Misty Terrain')) {
+ calls = 'Moonblast';
+ } else if (this.battle.hasPseudoWeather('Psychic Terrain')) {
+ calls = 'Psychic';
+ } else {
+ calls = 'Tri Attack';
+ }
+ } else if (this.battle.gen > 3) {
+ // In gens 4 and 5 it calls Earthquake.
+ calls = 'Earthquake';
+ } else {
+ // In gen 3 it calls Swift, so it retains its normal typing.
+ calls = 'Swift';
+ }
+ let calledMove = this.battle.dex.getMove(calls);
+ text += 'Calls ' + Dex.getTypeIcon(this.getMoveType(calledMove, value)[0]) + ' ' + calledMove.name;
+ }
+
+ text += 'Accuracy: ' + accuracy + '
';
+ if (zEffect) text += 'Z-Effect: ' + zEffect + '
';
+
+ if (this.battle.gen < 7 || this.battle.hardcoreMode) {
+ text += '' + move.shortDesc + '
';
+ } else {
+ text += '';
+ if (move.priority > 1) {
+ text += 'Nearly always moves first (priority +' + move.priority + ').
';
+ } else if (move.priority <= -1) {
+ text += 'Nearly always moves last (priority −' + (-move.priority) + ').
';
+ } else if (move.priority === 1) {
+ text += 'Usually moves first (priority +' + move.priority + ').
';
+ }
+
+ text += '' + (move.desc || move.shortDesc) + '
';
+
+ if (this.battle.gameType === 'doubles') {
+ if (move.target === 'allAdjacent') {
+ text += '◎ Hits both foes and ally.
';
+ } else if (move.target === 'allAdjacentFoes') {
+ text += '◎ Hits both foes.
';
+ }
+ } else if (this.battle.gameType === 'triples') {
+ if (move.target === 'allAdjacent') {
+ text += '◎ Hits adjacent foes and allies.
';
+ } else if (move.target === 'allAdjacentFoes') {
+ text += '◎ Hits adjacent foes.
';
+ } else if (move.target === 'any') {
+ text += '◎ Can target distant Pokémon in Triples.
';
+ }
+ }
+
+ if ('defrost' in move.flags) {
+ text += 'The user thaws out if it is frozen.
';
+ }
+ if (!('protect' in move.flags) && move.target !== 'self' && move.target !== 'allySide') {
+ text += 'Not blocked by Protect (and Detect, King\'s Shield, Spiky Shield)
';
+ }
+ if ('authentic' in move.flags) {
+ text += 'Bypasses Substitute (but does not break it)
';
+ }
+ if (!('reflectable' in move.flags) && move.target !== 'self' && move.target !== 'allySide' && move.category === 'Status') {
+ text += '✓ Not bounceable (can\'t be bounced by Magic Coat/Bounce)
';
+ }
+
+ if ('contact' in move.flags) {
+ text += '✓ Contact (triggers Iron Barbs, Spiky Shield, etc)
';
+ }
+ if ('sound' in move.flags) {
+ text += '✓ Sound (doesn\'t affect Soundproof pokemon)
';
+ }
+ if ('powder' in move.flags) {
+ text += '✓ Powder (doesn\'t affect Grass, Overcoat, Safety Goggles)
';
+ }
+ if ('punch' in move.flags && ability === 'ironfist') {
+ text += '✓ Fist (boosted by Iron Fist)
';
+ }
+ if ('pulse' in move.flags && ability === 'megalauncher') {
+ text += '✓ Pulse (boosted by Mega Launcher)
';
+ }
+ if ('bite' in move.flags && ability === 'strongjaw') {
+ text += '✓ Bite (boosted by Strong Jaw)
';
+ }
+ if ((move.recoil || move.hasCustomRecoil) && ability === 'reckless') {
+ text += '✓ Recoil (boosted by Reckless)
';
+ }
+ if ('bullet' in move.flags) {
+ text += '✓ Bullet-like (doesn\'t affect Bulletproof pokemon)
';
+ }
+ }
+ return text;
+ }
+
+ /**
+ * Needs either a Pokemon or a ServerPokemon, but note that neither
+ * are guaranteed: If you hover over a possible switch-in that's
+ * never been switched in before, you'll only have a ServerPokemon,
+ * and if you hover over an opponent's pokemon, you'll only have a
+ * Pokemon.
+ *
+ * isActive is true if hovering over a pokemon in the battlefield,
+ * and false if hovering over a pokemon in the Switch menu.
+ *
+ * @param clientPokemon
+ * @param serverPokemon
+ * @param isActive
+ */
+ showPokemonTooltip(clientPokemon: Pokemon | null, serverPokemon?: ServerPokemon | null, isActive?: boolean) {
+ const pokemon = clientPokemon || serverPokemon!;
+ let text = '';
+ let genderBuf = '';
+ if (pokemon.gender) {
+ genderBuf = '
';
+ }
+
+ let name = BattleLog.escapeHTML(pokemon.name);
+ if (pokemon.species !== pokemon.name) {
+ name += ' (' + BattleLog.escapeHTML(pokemon.species) + ')';
+ }
+
+ text += '' + name + genderBuf + (pokemon.level !== 100 ? ' L' + pokemon.level + '' : '') + '
';
+
+ let template = this.battle.dex.getTemplate(clientPokemon ? clientPokemon.getSpecies() : pokemon.species);
+ if (clientPokemon && clientPokemon.volatiles.formechange) {
+ if (clientPokemon.volatiles.transform) {
+ text += '(Transformed into ' + clientPokemon.volatiles.formechange[1] + ')
';
+ } else {
+ text += '(Changed forme: ' + clientPokemon.volatiles.formechange[1] + ')
';
+ }
+ }
+
+ let types = this.getPokemonTypes(pokemon);
+
+ if (clientPokemon && (clientPokemon.volatiles.typechange || clientPokemon.volatiles.typeadd)) {
+ text += '(Type changed)
';
+ }
+ text += types.map(type => Dex.getTypeIcon(type)).join(' ');
+ text += '
';
+
+ if (pokemon.fainted) {
+ text += 'HP: (fainted)
';
+ } else if (this.battle.hardcoreMode) {
+ if (serverPokemon) {
+ text += 'HP: ' + serverPokemon.hp + '/' + serverPokemon.maxhp + (pokemon.status ? ' ' + pokemon.status.toUpperCase() + '' : '') + '
';
+ }
+ } else {
+ let exacthp = '';
+ if (serverPokemon) {
+ exacthp = ' (' + serverPokemon.hp + '/' + serverPokemon.maxhp + ')';
+ } else if (pokemon.maxhp === 48) {
+ exacthp = ' (' + pokemon.hp + '/' + pokemon.maxhp + ' pixels)';
+ }
+ text += 'HP: ' + Pokemon.getHPText(pokemon) + exacthp + (pokemon.status ? ' ' + pokemon.status.toUpperCase() + '' : '');
+ if (clientPokemon) {
+ if (pokemon.status === 'tox') {
+ if (pokemon.ability === 'Poison Heal' || pokemon.ability === 'Magic Guard') {
+ text += ' Would take if ability removed: ' + Math.floor(100 / 16) * Math.min(clientPokemon.statusData.toxicTurns + 1, 15) + '%';
+ } else {
+ text += ' Next damage: ' + Math.floor(100 / 16) * Math.min(clientPokemon.statusData.toxicTurns + 1, 15) + '%';
+ }
+ } else if (pokemon.status === 'slp') {
+ text += ' Turns asleep: ' + clientPokemon.statusData.sleepTurns;
+ }
+ }
+ text += '
';
+ }
+
+ const supportsAbilities = this.battle.gen > 2 && !this.battle.tier.includes("Let's Go");
+ if (serverPokemon) {
+ if (supportsAbilities) {
+ let abilityText = Dex.getAbility(serverPokemon.baseAbility).name;
+ let ability = Dex.getAbility(serverPokemon.ability || pokemon.ability).name;
+ if (ability && (ability !== abilityText)) {
+ abilityText = ability + ' (base: ' + abilityText + ')';
+ }
+ text += 'Ability: ' + abilityText;
+ if (serverPokemon.item) {
+ text += ' / Item: ' + Dex.getItem(serverPokemon.item).name;
+ }
+ text += '
';
+ } else if (serverPokemon.item) {
+ let itemName = Dex.getItem(serverPokemon.item).name;
+ text += 'Item: ' + itemName + '
';
+ }
+ } else if (clientPokemon) {
+ if (supportsAbilities) {
+ if (!pokemon.baseAbility && !pokemon.ability) {
+ let abilities = template.abilities;
+ text += 'Possible abilities: ' + abilities['0'];
+ if (abilities['1']) text += ', ' + abilities['1'];
+ if (abilities['H']) text += ', ' + abilities['H'];
+ if (abilities['S']) text += ', ' + abilities['S'];
+ text += '
';
+ } else if (pokemon.ability) {
+ if (pokemon.ability === pokemon.baseAbility) {
+ text += 'Ability: ' + Dex.getAbility(pokemon.ability).name + '
';
+ } else {
+ text += 'Ability: ' + Dex.getAbility(pokemon.ability).name + ' (base: ' + Dex.getAbility(pokemon.baseAbility).name + ')' + '
';
+ }
+ } else if (pokemon.baseAbility) {
+ text += 'Ability: ' + Dex.getAbility(pokemon.baseAbility).name + '
';
+ }
+ }
+ let item = '';
+ let itemEffect = clientPokemon.itemEffect || '';
+ if (clientPokemon.prevItem) {
+ item = 'None';
+ if (itemEffect) itemEffect += '; ';
+ let prevItem = Dex.getItem(clientPokemon.prevItem).name;
+ itemEffect += clientPokemon.prevItemEffect ? prevItem + ' was ' + clientPokemon.prevItemEffect : 'was ' + prevItem;
+ }
+ if (pokemon.item) item = Dex.getItem(pokemon.item).name;
+ if (itemEffect) itemEffect = ' (' + itemEffect + ')';
+ if (item) text += 'Item: ' + item + itemEffect + '
';
+ }
+
+ text += this.renderStats(clientPokemon, serverPokemon, !isActive);
+
+ if (serverPokemon && !isActive) {
+ // move list
+ text += '';
+ let battlePokemon = this.battle.getPokemon(pokemon.ident, pokemon.details);
+ for (const moveid of serverPokemon.moves) {
+ let move = Dex.getMove(moveid);
+ let moveName = move.name;
+ if (battlePokemon && battlePokemon.moveTrack) {
+ for (const row of battlePokemon.moveTrack) {
+ if (moveName === row[0]) {
+ moveName = this.getPPUseText(row, true);
+ break;
+ }
+ }
+ }
+ text += '• ' + moveName + '
';
+ }
+ text += '
';
+ } else if (!this.battle.hardcoreMode && clientPokemon && clientPokemon.moveTrack.length) {
+ // move list (guessed)
+ text += '';
+ for (const row of clientPokemon.moveTrack) {
+ text += '• ' + this.getPPUseText(row) + '
';
+ }
+ if (clientPokemon.moveTrack.length > 4) {
+ text += '(More than 4 moves is usually a sign of Illusion Zoroark/Zorua.)';
+ }
+ if (this.battle.gen === 3) {
+ text += '(Pressure is not visible in Gen 3, so in certain situations, more PP may have been lost than shown here.)';
+ }
+ text += '
';
+ }
+ return text;
+ }
+
+ calculateModifiedStats(clientPokemon: Pokemon | null, serverPokemon: ServerPokemon) {
+ let stats = {...serverPokemon.stats};
+ let pokemon = clientPokemon || serverPokemon;
+ for (const statName of Dex.statNamesExceptHP) {
+ stats[statName] = serverPokemon.stats[statName];
+
+ if (clientPokemon && clientPokemon.boosts[statName]) {
+ let boostTable = [1, 1.5, 2, 2.5, 3, 3.5, 4];
+ if (clientPokemon.boosts[statName] > 0) {
+ stats[statName] *= boostTable[clientPokemon.boosts[statName]];
+ } else {
+ if (this.battle.gen <= 2) boostTable = [1, 100 / 66, 2, 2.5, 100 / 33, 100 / 28, 4];
+ stats[statName] /= boostTable[-clientPokemon.boosts[statName]];
+ }
+ stats[statName] = Math.floor(stats[statName]);
+ }
+ }
+
+ let ability = toId(serverPokemon.ability || pokemon.ability || serverPokemon.baseAbility);
+ if (clientPokemon && 'gastroacid' in clientPokemon.volatiles) ability = '' as ID;
+
+ // check for burn, paralysis, guts, quick feet
+ if (pokemon.status) {
+ if (this.battle.gen > 2 && ability === 'guts') {
+ stats.atk = Math.floor(stats.atk * 1.5);
+ } else if (pokemon.status === 'brn') {
+ stats.atk = Math.floor(stats.atk * 0.5);
+ }
+
+ if (this.battle.gen > 2 && ability === 'quickfeet') {
+ stats.spe = Math.floor(stats.spe * 1.5);
+ } else if (pokemon.status === 'par') {
+ if (this.battle.gen > 6) {
+ stats.spe = Math.floor(stats.spe * 0.5);
+ } else {
+ stats.spe = Math.floor(stats.spe * 0.25);
+ }
+ }
+ }
+
+ // gen 1 doesn't support items
+ if (this.battle.gen <= 1) {
+ for (const statName of Dex.statNamesExceptHP) {
+ if (stats[statName] > 999) stats[statName] = 999;
+ }
+ return stats;
+ }
+
+ let item = toId(serverPokemon.item);
+ if (ability === 'klutz' && item !== 'machobrace') item = '' as ID;
+ let species = Dex.getTemplate(clientPokemon ? clientPokemon.getSpecies() : serverPokemon.species).baseSpecies;
+
+ // check for light ball, thick club, metal/quick powder
+ // the only stat modifying items in gen 2 were light ball, thick club, metal powder
+ if (item === 'lightball' && species === 'Pikachu') {
+ if (this.battle.gen >= 4) stats.atk *= 2;
+ stats.spa *= 2;
+ }
+
+ if (item === 'thickclub') {
+ if (species === 'Marowak' || species === 'Cubone') {
+ stats.atk *= 2;
+ }
+ }
+
+ if (species === 'Ditto' && !(clientPokemon && 'transform' in clientPokemon.volatiles)) {
+ if (item === 'quickpowder') {
+ stats.spe *= 2;
+ }
+ if (item === 'metalpowder') {
+ if (this.battle.gen === 2) {
+ stats.def = Math.floor(stats.def * 1.5);
+ stats.spd = Math.floor(stats.spd * 1.5);
+ } else {
+ stats.def *= 2;
+ }
+ }
+ }
+
+ // check abilities other than Guts and Quick Feet
+ // check items other than light ball, thick club, metal/quick powder
+ if (this.battle.gen <= 2) {
+ return stats;
+ }
+
+ let weather = this.battle.weather;
+ if (weather) {
+ // Check if anyone has an anti-weather ability
+ outer: for (const side of this.battle.sides) {
+ for (const active of side.active) {
+ if (active && ['Air Lock', 'Cloud Nine'].includes(active.ability)) {
+ weather = '' as ID;
+ break outer;
+ }
+ }
+ }
+ }
+
+ if (item === 'choiceband') {
+ stats.atk = Math.floor(stats.atk * 1.5);
+ }
+ if (ability === 'purepower' || ability === 'hugepower') {
+ stats.atk *= 2;
+ }
+ if (ability === 'hustle') {
+ stats.atk = Math.floor(stats.atk * 1.5);
+ }
+ if (weather) {
+ if (weather === 'sunnyday' || weather === 'desolateland') {
+ if (ability === 'solarpower') {
+ stats.spa = Math.floor(stats.spa * 1.5);
+ }
+ let allyActive = clientPokemon && clientPokemon.side.active;
+ if (allyActive && allyActive.length > 1) {
+ for (const ally of allyActive) {
+ if (!ally || ally.fainted) continue;
+ if (ally.ability === 'flowergift' && (ally.getTemplate().baseSpecies === 'Cherrim' || this.battle.gen <= 4)) {
+ stats.atk = Math.floor(stats.atk * 1.5);
+ stats.spd = Math.floor(stats.spd * 1.5);
+ }
+ }
+ }
+ }
+ if (this.battle.gen >= 4 && this.pokemonHasType(serverPokemon, 'Rock') && weather === 'sandstorm') {
+ stats.spd = Math.floor(stats.spd * 1.5);
+ }
+ if (ability === 'chlorophyll' && (weather === 'sunnyday' || weather === 'desolateland')) {
+ stats.spe *= 2;
+ }
+ if (ability === 'swiftswim' && (weather === 'raindance' || weather === 'primordialsea')) {
+ stats.spe *= 2;
+ }
+ if (ability === 'sandrush' && weather === 'sandstorm') {
+ stats.spe *= 2;
+ }
+ if (ability === 'slushrush' && weather === 'hail') {
+ stats.spe *= 2;
+ }
+ }
+ if (ability === 'defeatist' && serverPokemon.hp <= serverPokemon.maxhp / 2) {
+ stats.atk = Math.floor(stats.atk * 0.5);
+ stats.spa = Math.floor(stats.spa * 0.5);
+ }
+ if (clientPokemon) {
+ if ('slowstart' in clientPokemon.volatiles) {
+ stats.atk = Math.floor(stats.atk * 0.5);
+ stats.spe = Math.floor(stats.spe * 0.5);
+ }
+ if (ability === 'unburden' && 'itemremoved' in clientPokemon.volatiles && !item) {
+ stats.spe *= 2;
+ }
+ }
+ if (ability === 'marvelscale' && pokemon.status) {
+ stats.def = Math.floor(stats.def * 1.5);
+ }
+ if (item === 'eviolite' && Dex.getTemplate(pokemon.species).evos) {
+ stats.def = Math.floor(stats.def * 1.5);
+ stats.spd = Math.floor(stats.spd * 1.5);
+ }
+ if (ability === 'grasspelt' && this.battle.hasPseudoWeather('Grassy Terrain')) {
+ stats.def = Math.floor(stats.def * 1.5);
+ }
+ if (ability === 'surgesurfer' && this.battle.hasPseudoWeather('Electric Terrain')) {
+ stats.spe *= 2;
+ }
+ if (item === 'choicespecs') {
+ stats.spa = Math.floor(stats.spa * 1.5);
+ }
+ if (item === 'deepseatooth' && species === 'Clamperl') {
+ stats.spa *= 2;
+ }
+ if (item === 'souldew' && this.battle.gen <= 6 && (species === 'Latios' || species === 'Latias')) {
+ stats.spa = Math.floor(stats.spa * 1.5);
+ stats.spd = Math.floor(stats.spd * 1.5);
+ }
+ if (clientPokemon && (ability === 'plus' || ability === 'minus')) {
+ let allyActive = clientPokemon.side.active;
+ if (allyActive.length > 1) {
+ let abilityName = (ability === 'plus' ? 'Plus' : 'Minus');
+ for (const ally of allyActive) {
+ if (!(ally && ally !== clientPokemon && !ally.fainted)) continue;
+ if (!(ally.ability === 'Plus' || ally.ability === 'Minus')) continue;
+ if (this.battle.gen <= 4 && ally.ability === abilityName) continue;
+ stats.spa = Math.floor(stats.spa * 1.5);
+ break;
+ }
+ }
+ }
+ if (item === 'assaultvest') {
+ stats.spd = Math.floor(stats.spd * 1.5);
+ }
+ if (item === 'deepseascale' && species === 'Clamperl') {
+ stats.spd *= 2;
+ }
+ if (item === 'choicescarf') {
+ stats.spe = Math.floor(stats.spe * 1.5);
+ }
+ if (item === 'ironball' || item === 'machobrace' || /power(?!herb)/.test(item)) {
+ stats.spe = Math.floor(stats.spe * 0.5);
+ }
+ if (ability === 'furcoat') {
+ stats.def *= 2;
+ }
+
+ return stats;
+ }
+
+ renderStats(clientPokemon: Pokemon | null, serverPokemon?: ServerPokemon | null, short?: boolean) {
+ if (!serverPokemon) {
+ if (!clientPokemon) throw new Error('Must pass either clientPokemon or serverPokemon');
+ let [min, max] = this.getSpeedRange(clientPokemon);
+ return 'Spe ' + min + ' to ' + max + ' (before items/abilities/modifiers)
';
+ }
+ const stats = serverPokemon.stats;
+ const modifiedStats = this.calculateModifiedStats(clientPokemon, serverPokemon);
+
+ let buf = '';
+
+ if (!short) {
+ let hasModifiedStat = false;
+ for (const statName of Dex.statNamesExceptHP) {
+ if (this.battle.gen === 1 && statName === 'spd') continue;
+ let statLabel = this.battle.gen === 1 && statName === 'spa' ? 'spc' : statName;
+ buf += statName === 'atk' ? '' : ' / ';
+ buf += '' + BattleText[statLabel].statShortName + ' ';
+ buf += '' + stats[statName];
+ if (modifiedStats[statName] !== stats[statName]) hasModifiedStat = true;
+ }
+ buf += '
';
+
+ if (!hasModifiedStat) return buf;
+
+ buf += '(After stat modifiers:)
';
+ buf += '';
+ }
+
+ for (const statName of Dex.statNamesExceptHP) {
+ if (this.battle.gen === 1 && statName === 'spd') continue;
+ let statLabel = this.battle.gen === 1 && statName === 'spa' ? 'spc' : statName;
+ buf += statName === 'atk' ? '' : ' / ';
+ buf += '' + BattleText[statLabel].statShortName + ' ';
+ if (modifiedStats[statName] === stats[statName]) {
+ buf += '' + modifiedStats[statName];
+ } else if (modifiedStats[statName] < stats[statName]) {
+ buf += '' + modifiedStats[statName] + '';
+ } else {
+ buf += '' + modifiedStats[statName] + '';
+ }
+ }
+ buf += '
';
+ return buf;
+ }
+
+ getPPUseText(moveTrackRow: [string, number], showKnown?: boolean) {
+ let [moveName, ppUsed] = moveTrackRow;
+ let move;
+ let maxpp;
+ if (moveName.charAt(0) === '*') {
+ // Transformed move
+ move = this.battle.dex.getMove(moveName.substr(1));
+ maxpp = 5;
+ } else {
+ move = this.battle.dex.getMove(moveName);
+ maxpp = move.noPPBoosts ? move.pp : Math.floor(move.pp * 8 / 5);
+ }
+ if (ppUsed === Infinity) {
+ return move.name + ' (0/' + maxpp + ')';
+ }
+ if (ppUsed || moveName.charAt(0) === '*') {
+ return move.name + ' (' + (maxpp - ppUsed) + '/' + maxpp + ')';
+ }
+ return move.name + (showKnown ? ' (revealed)' : '');
+ }
+
+ ppUsed(move: Move, pokemon: Pokemon) {
+ for (let [moveName, ppUsed] of pokemon.moveTrack) {
+ if (moveName.charAt(0) === '*') moveName = moveName.substr(1);
+ if (move.name === moveName) return ppUsed;
+ }
+ return 0;
+ }
+
+ /**
+ * Calculates possible Speed stat range of an opponent
+ */
+ getSpeedRange(pokemon: Pokemon): [number, number] {
+ let level = pokemon.level;
+ let baseSpe = pokemon.getTemplate().baseStats['spe'];
+ let tier = this.battle.tier;
+ let gen = this.battle.gen;
+ let isRandomBattle = tier.includes('Random Battle') || (tier.includes('Random') && tier.includes('Battle') && gen >= 6);
+
+ let minNature = (isRandomBattle || gen < 3) ? 1 : 0.9;
+ let maxNature = (isRandomBattle || gen < 3) ? 1 : 1.1;
+ let maxIv = (gen < 3) ? 30 : 31;
+
+ let min;
+ let max;
+ const tr = Math.trunc || Math.floor;
+ if (tier.includes("Let's Go")) {
+ min = tr(tr(tr(2 * baseSpe * level / 100 + 5) * minNature) * tr((70 / 255 / 10 + 1) * 100) / 100);
+ max = tr(tr(tr((2 * baseSpe + maxIv) * level / 100 + 5) * maxNature) * tr((70 / 255 / 10 + 1) * 100) / 100);
+ if (tier.includes('No Restrictions')) max += 200;
+ else if (tier.includes('Random')) max += 20;
+ } else {
+ let maxIvEvOffset = maxIv + ((isRandomBattle && gen >= 3) ? 21 : 63);
+ min = tr(tr(2 * baseSpe * level / 100 + 5) * minNature);
+ max = tr(tr((2 * baseSpe + maxIvEvOffset) * level / 100 + 5) * maxNature);
+ }
+ return [min, max];
+ }
+
+ /**
+ * Gets the proper current type for moves with a variable type.
+ */
+ getMoveType(move: Move, value: ModifiableValue): [TypeName, 'Physical' | 'Special' | 'Status'] {
+ let moveType = move.type;
+ let category = move.category;
+ let pokemonTypes = value.pokemon!.getTypeList(value.serverPokemon);
+ value.reset();
+ if (move.id === 'revelationdance') {
+ moveType = pokemonTypes[0];
+ }
+ // Moves that require an item to change their type.
+ let item = Dex.getItem(value.itemName);
+ if (move.id === 'multiattack' && item.onMemory) {
+ if (value.itemModify(0)) moveType = item.onMemory;
+ }
+ if (move.id === 'judgment' && item.onPlate) {
+ if (value.itemModify(0)) moveType = item.onPlate;
+ }
+ if (move.id === 'technoblast' && item.onDrive) {
+ if (value.itemModify(0)) moveType = item.onDrive;
+ }
+ if (move.id === 'naturalgift' && item.naturalGift) {
+ if (value.itemModify(0)) moveType = item.naturalGift.type;
+ }
+ // Weather and pseudo-weather type changes.
+ if (move.id === 'weatherball' && value.weatherModify(0)) {
+ switch (this.battle.weather) {
+ case 'sunnyday': case 'desolateland': moveType = 'Fire'; break;
+ case 'raindance': case 'primordialsea': moveType = 'Water'; break;
+ case 'sandstorm': moveType = 'Rock'; break;
+ case 'hail': moveType = 'Ice'; break;
+ }
+ }
+ // Other abilities that change the move type.
+ const noTypeOverride = ['judgment', 'multiattack', 'naturalgift', 'revelationdance', 'struggle', 'technoblast', 'weatherball'];
+ const allowTypeOverride = !noTypeOverride.includes(move.id);
+
+ if (allowTypeOverride && move.flags['sound'] && value.abilityModify(0, 'Liquid Voice')) {
+ moveType = 'Water';
+ }
+ if (allowTypeOverride && moveType === 'Normal' && category !== 'Status') {
+ if (value.abilityModify(0, 'Aerilate')) moveType = 'Flying';
+ if (value.abilityModify(0, 'Galvanize')) moveType = 'Electric';
+ if (value.abilityModify(0, 'Pixilate')) moveType = 'Fairy';
+ if (value.abilityModify(0, 'Refrigerate')) moveType = 'Ice';
+ if (value.abilityModify(0, 'Normalize')) moveType = 'Normal';
+ }
+ if (this.battle.gen <= 3 && category !== 'Status') {
+ category = Dex.getGen3Category(moveType);
+ }
+ return [moveType, category];
+ }
+
+ // Gets the current accuracy for a move.
+ getMoveAccuracy(move: Move, value: ModifiableValue, target?: Pokemon) {
+ value.reset(move.accuracy === true ? 0 : move.accuracy, true);
+
+ let pokemon = value.pokemon!;
+ if (move.id === 'toxic' && this.battle.gen >= 6 && this.pokemonHasType(pokemon, 'Poison')) {
+ value.set(0, "Poison type");
+ return value;
+ }
+ if (move.id === 'blizzard') {
+ value.weatherModify(0, 'Hail');
+ }
+ if (move.id === 'hurricane' || move.id === 'thunder') {
+ value.weatherModify(0, 'Rain Dance');
+ value.weatherModify(0, 'Primordial Sea');
+ if (value.tryWeather('Sunny Day')) value.set(50, 'Sunny Day');
+ if (value.tryWeather('Desolate Land')) value.set(50, 'Desolate Land');
+ }
+ value.abilityModify(0, 'No Guard');
+ if (!value.value) return value;
+ if (move.ohko) {
+ if (this.battle.gen === 1) {
+ value.set(value.value, `fails if target's Speed is higher`);
+ return value;
+ }
+ if (move.id === 'sheercold' && this.battle.gen >= 7) {
+ if (!this.pokemonHasType(pokemon, 'Ice')) value.set(20, 'not Ice-type');
+ }
+ if (target) {
+ if (pokemon.level < target.level) {
+ value.reset(0);
+ value.set(0, "FAILS: target's level is higher");
+ } else if (pokemon.level > target.level) {
+ value.set(value.value + pokemon.level - target.level, "+1% per level above target");
+ }
+ } else {
+ if (pokemon.level < 100) value.set(value.value, "fails if target's level is higher");
+ if (pokemon.level > 1) value.set(value.value, "+1% per level above target");
+ }
+ return value;
+ }
+ if (pokemon && pokemon.boosts.accuracy) {
+ if (pokemon.boosts.accuracy > 0) {
+ value.modify((pokemon.boosts.accuracy + 3) / 3);
+ } else {
+ value.modify(3 / (3 - pokemon.boosts.accuracy));
+ }
+ }
+ if (move.category === 'Physical') {
+ value.abilityModify(0.8, "Hustle");
+ }
+ value.abilityModify(1.3, "Compound Eyes");
+ for (const active of pokemon.side.active) {
+ if (!active || active.fainted) continue;
+ let ability = Dex.getAbility(active.ability).name;
+ if (ability === 'Victory Star') {
+ value.modify(1.1, "Victory Star");
+ }
+ }
+ value.itemModify(1.1, "Wide Lens");
+ if (this.battle.hasPseudoWeather('Gravity')) {
+ value.modify(5 / 3, "Gravity");
+ }
+ return value;
+ }
+
+ // Gets the proper current base power for moves which have a variable base power.
+ // Takes into account the target for some moves.
+ // If it is unsure of the actual base power, it gives an estimate.
+ getMoveBasePower(move: Move, moveType: TypeName, value: ModifiableValue, target: Pokemon | null = null) {
+ const pokemon = value.pokemon!;
+ const serverPokemon = value.serverPokemon;
+
+ value.reset(move.basePower);
+
+ if (move.id === 'acrobatics') {
+ if (!serverPokemon.item) {
+ value.modify(2, "Acrobatics + no item");
+ }
+ }
+ if (['crushgrip', 'wringout'].includes(move.id) && target) {
+ value.set(
+ Math.floor(Math.floor((120 * (100 * Math.floor(target.hp * 4096 / target.maxhp)) + 2048 - 1) / 4096) / 100) || 1,
+ 'approximate'
+ );
+ }
+ if (move.id === 'brine' && target && target.hp * 2 <= target.maxhp) {
+ value.modify(2, 'Brine + target below half HP');
+ }
+ if (move.id === 'eruption' || move.id === 'waterspout') {
+ value.set(Math.floor(150 * pokemon.hp / pokemon.maxhp) || 1);
+ }
+ if (move.id === 'facade' && !['', 'slp', 'frz'].includes(pokemon.status)) {
+ value.modify(2, 'Facade + status');
+ }
+ if (move.id === 'flail' || move.id === 'reversal') {
+ let multiplier;
+ let ratios;
+ if (this.battle.gen > 4) {
+ multiplier = 48;
+ ratios = [2, 5, 10, 17, 33];
+ } else {
+ multiplier = 64;
+ ratios = [2, 6, 13, 22, 43];
+ }
+ let ratio = pokemon.hp * multiplier / pokemon.maxhp;
+ let basePower;
+ if (ratio < ratios[0]) basePower = 200;
+ else if (ratio < ratios[1]) basePower = 150;
+ else if (ratio < ratios[2]) basePower = 100;
+ else if (ratio < ratios[3]) basePower = 80;
+ else if (ratio < ratios[4]) basePower = 40;
+ else basePower = 20;
+ value.set(basePower);
+ }
+ if (move.id === 'hex' && target && target.status) {
+ value.modify(2, 'Hex + status');
+ }
+ if (move.id === 'punishment' && target) {
+ let boostCount = 0;
+ for (const boost of Object.values(target.boosts)) {
+ if (boost > 0) boostCount += boost;
+ }
+ value.set(Math.min(60 + 20 * boostCount, 200));
+ }
+ if (move.id === 'smellingsalts' && target) {
+ if (target.status === 'par') {
+ value.modify(2, 'Smelling Salts + Paralysis');
+ }
+ }
+ if (['storedpower', 'powertrip'].includes(move.id) && target) {
+ let boostCount = 0;
+ for (const boost of Object.values(target.boosts)) {
+ if (boost > 0) boostCount += boost;
+ }
+ value.set(20 + 20 * boostCount);
+ }
+ if (move.id === 'trumpcard') {
+ const ppLeft = 5 - this.ppUsed(move, pokemon);
+ let basePower = 40;
+ if (ppLeft === 1) basePower = 200;
+ else if (ppLeft === 2) basePower = 80;
+ else if (ppLeft === 3) basePower = 60;
+ else if (ppLeft === 4) basePower = 50;
+ value.set(basePower);
+ }
+ if (move.id === 'venoshock' && target) {
+ if (['psn', 'tox'].includes(target.status)) {
+ value.modify(2, 'Venoshock + Poison');
+ }
+ }
+ if (move.id === 'wakeupslap' && target) {
+ if (target.status === 'slp') {
+ value.modify(2, 'Wake-Up Slap + Sleep');
+ }
+ }
+ if (move.id === 'weatherball') {
+ value.weatherModify(2);
+ }
+ if (move.id === 'watershuriken' && pokemon.getSpecies() === 'Greninja-Ash' && pokemon.ability === 'Battle Bond') {
+ value.set(20, 'Battle Bond');
+ }
+ // Moves that check opponent speed
+ if (move.id === 'electroball' && target) {
+ let [minSpe, maxSpe] = this.getSpeedRange(target);
+ let minRatio = (serverPokemon.stats['spe'] / maxSpe);
+ let maxRatio = (serverPokemon.stats['spe'] / minSpe);
+ let min;
+ let max;
+
+ if (minRatio >= 4) min = 150;
+ else if (minRatio >= 3) min = 120;
+ else if (minRatio >= 2) min = 80;
+ else if (minRatio >= 1) min = 60;
+ else min = 40;
+
+ if (maxRatio >= 4) max = 150;
+ else if (maxRatio >= 3) max = 120;
+ else if (maxRatio >= 2) max = 80;
+ else if (maxRatio >= 1) max = 60;
+ else max = 40;
+
+ value.setRange(min, max);
+ }
+ if (move.id === 'gyroball' && target) {
+ let [minSpe, maxSpe] = this.getSpeedRange(target);
+ let min = (Math.floor(25 * minSpe / serverPokemon.stats['spe']) || 1);
+ if (min > 150) min = 150;
+ let max = (Math.floor(25 * maxSpe / serverPokemon.stats['spe']) || 1);
+ if (max > 150) max = 150;
+ value.setRange(min, max);
+ }
+ // Moves which have base power changed due to items
+ if (serverPokemon.item) {
+ let item = Dex.getItem(serverPokemon.item);
+ if (move.id === 'fling' && item.fling) {
+ value.itemModify(item.fling.basePower);
+ }
+ if (move.id === 'naturalgift') {
+ value.itemModify(item.naturalGift.basePower);
+ }
+ }
+ // Moves which have base power changed according to weight
+ if (['lowkick', 'grassknot', 'heavyslam', 'heatcrash'].includes(move.id)) {
+ let isGKLK = ['lowkick', 'grassknot'].includes(move.id);
+ if (target) {
+ let targetWeight = target.getWeightKg();
+ let pokemonWeight = pokemon.getWeightKg(serverPokemon);
+ let basePower;
+ if (isGKLK) {
+ basePower = 20;
+ if (targetWeight >= 200) basePower = 120;
+ else if (targetWeight >= 100) basePower = 100;
+ else if (targetWeight >= 50) basePower = 80;
+ else if (targetWeight >= 25) basePower = 60;
+ else if (targetWeight >= 10) basePower = 40;
+ } else {
+ basePower = 40;
+ if (pokemonWeight > targetWeight * 5) basePower = 120;
+ else if (pokemonWeight > targetWeight * 4) basePower = 100;
+ else if (pokemonWeight > targetWeight * 3) basePower = 80;
+ else if (pokemonWeight > targetWeight * 2) basePower = 60;
+ }
+ value.set(basePower);
+ } else {
+ value.setRange(isGKLK ? 20 : 40, 120);
+ }
+ }
+ if (!value.value) return value;
+
+ // Other ability boosts
+ if (pokemon.status === 'brn' && move.category === 'Special') {
+ value.abilityModify(1.5, "Flare Boost");
+ }
+ if (move.flags['pulse']) {
+ value.abilityModify(1.5, "Mega Launcher");
+ }
+ if (move.flags['bite']) {
+ value.abilityModify(1.5, "Strong Jaw");
+ }
+ if (value.value <= 60) {
+ value.abilityModify(1.5, "Technician");
+ }
+ if (['psn', 'tox'].includes(pokemon.status) && move.category === 'Physical') {
+ value.abilityModify(1.5, "Toxic Boost");
+ }
+ if (['Rock', 'Ground', 'Steel'].includes(moveType) && this.battle.weather === 'sandstorm') {
+ if (value.tryAbility("Sand Force")) value.weatherModify(1.3, "Sandstorm", "Sand Force");
+ }
+ if (move.secondaries) {
+ value.abilityModify(1.3, "Sheer Force");
+ }
+ if (move.flags['contact']) {
+ value.abilityModify(1.3, "Tough Claws");
+ }
+ if (target) {
+ if (["MF", "FM"].includes(pokemon.gender + target.gender)) {
+ value.abilityModify(1.25, "Rivalry");
+ } else if (["MM", "FF"].includes(pokemon.gender + target.gender)) {
+ value.abilityModify(0.75, "Rivalry");
+ }
+ }
+ const noTypeOverride = ['judgment', 'multiattack', 'naturalgift', 'revelationdance', 'struggle', 'technoblast', 'weatherball'];
+ if (move.type === 'Normal' && move.category !== 'Status' && !noTypeOverride.includes(move.id)) {
+ value.abilityModify(this.battle.gen > 6 ? 1.2 : 1.3, "Aerilate");
+ value.abilityModify(this.battle.gen > 6 ? 1.2 : 1.3, "Galvanize");
+ value.abilityModify(this.battle.gen > 6 ? 1.2 : 1.3, "Pixilate");
+ value.abilityModify(this.battle.gen > 6 ? 1.2 : 1.3, "Refrigerate");
+ if (this.battle.gen > 6) {
+ value.abilityModify(1.2, "Normalize");
+ }
+ }
+ if (move.flags['punch']) {
+ value.abilityModify(1.2, 'Iron Fist');
+ }
+ if (move.recoil || move.hasCustomRecoil) {
+ value.abilityModify(1.2, 'Reckless');
+ }
+
+ if (move.category !== 'Status') {
+ let auraBoosted = '';
+ let auraBroken = false;
+ for (const ally of pokemon.side.active) {
+ if (!ally || ally.fainted) continue;
+ if (moveType === 'Fairy' && ally.ability === 'Fairy Aura') {
+ auraBoosted = 'Fairy Aura';
+ } else if (moveType === 'Dark' && ally.ability === 'Dark Aura') {
+ auraBoosted = 'Dark Aura';
+ } else if (ally.ability === 'Aura Break') {
+ auraBroken = true;
+ } else if (ally.ability === 'Battery') {
+ if (ally !== pokemon && move.category === 'Special') {
+ value.modify(1.3, 'Battery');
+ }
+ }
+ }
+ for (const foe of pokemon.side.foe.active) {
+ if (!foe || foe.fainted) continue;
+ if (foe.ability === 'Fairy Aura') {
+ if (moveType === 'Fairy') auraBoosted = 'Fairy Aura';
+ } else if (foe.ability === 'Dark Aura') {
+ if (moveType === 'Dark') auraBoosted = 'Dark Aura';
+ } else if (foe.ability === 'Aura Break') {
+ auraBroken = true;
+ }
+ }
+ if (auraBoosted) {
+ if (auraBroken) {
+ value.modify(0.75, auraBoosted + ' + Aura Break');
+ } else {
+ value.modify(1.33, auraBoosted);
+ }
+ }
+ }
+
+ // Terrain
+ if ((this.battle.hasPseudoWeather('Electric Terrain') && moveType === 'Electric') ||
+ (this.battle.hasPseudoWeather('Grassy Terrain') && moveType === 'Grass') ||
+ (this.battle.hasPseudoWeather('Psychic Terrain') && moveType === 'Psychic')) {
+ if (pokemon.isGrounded(serverPokemon)) {
+ value.modify(1.5, 'Terrain boost');
+ }
+ } else if (this.battle.hasPseudoWeather('Misty Terrain') && moveType === 'Dragon') {
+ if (target ? target.isGrounded() : true) {
+ value.modify(0.5, 'Misty Terrain + grounded target');
+ }
+ }
+
+ return value;
+ }
+
+ static incenseTypes: {[itemName: string]: TypeName} = {
+ 'Odd Incense': 'Psychic',
+ 'Rock Incense': 'Rock',
+ 'Rose Incense': 'Grass',
+ 'Sea Incense': 'Water',
+ 'Wave Incense': 'Water',
+ };
+ static itemTypes: {[itemName: string]: TypeName} = {
+ 'Black Belt': 'Fighting',
+ 'Black Glasses': 'Dark',
+ 'Charcoal': 'Fire',
+ 'Dragon Fang': 'Dragon',
+ 'Hard Stone': 'Rock',
+ 'Magnet': 'Electric',
+ 'Metal Coat': 'Steel',
+ 'Miracle Seed': 'Grass',
+ 'Mystic Water': 'Water',
+ 'Never-Melt Ice': 'Ice',
+ 'Poison Barb': 'Poison',
+ 'Sharp Beak': 'Flying',
+ 'Silk Scarf': 'Normal',
+ 'SilverPowder': 'Bug',
+ 'Soft Sand': 'Ground',
+ 'Spell Tag': 'Ghost',
+ 'Twisted Spoon': 'Psychic',
+ };
+ static orbUsers: {[speciesName: string]: string} = {
+ 'Latias': 'Soul Dew',
+ 'Latios': 'Soul Dew',
+ 'Dialga': 'Adamant Orb',
+ 'Palkia': 'Lustrous Orb',
+ 'Giratina': 'Griseous Orb',
+ };
+ static orbTypes: {[itemName: string]: TypeName} = {
+ 'Soul Dew': 'Psychic',
+ 'Adamant Orb': 'Steel',
+ 'Lustrous Orb': 'Water',
+ 'Griseous Orb': 'Ghost',
+ };
+ static noGemMoves = [
+ 'Fire Pledge',
+ 'Fling',
+ 'Grass Pledge',
+ 'Struggle',
+ 'Water Pledge',
+ ];
+ getItemBoost(move: Move, value: ModifiableValue, moveType: TypeName) {
+ let item = this.battle.dex.getItem(value.serverPokemon.item);
+ let itemName = item.name;
+ let moveName = move.name;
+
+ // Plates
+ if (item.onPlate === moveType && !item.zMove) {
+ value.itemModify(1.2);
+ return value;
+ }
+
+ // Incenses
+ if (BattleTooltips.incenseTypes[item.name] === moveType) {
+ value.itemModify(1.2);
+ return value;
+ }
+
+ // Type-enhancing items
+ if (BattleTooltips.itemTypes[item.name] === moveType) {
+ value.itemModify(this.battle.gen < 4 ? 1.1 : 1.2);
+ return value;
+ }
+
+ // Pokemon-specific items
+ if (item.name === 'Soul Dew' && this.battle.gen < 7) return value;
+ if (BattleTooltips.orbUsers[Dex.getTemplate(value.serverPokemon.species).baseSpecies] === item.name &&
+ [BattleTooltips.orbTypes[item.name], 'Dragon'].includes(moveType)) {
+ value.itemModify(1.2);
+ return value;
+ }
+
+ // Gems
+ if (BattleTooltips.noGemMoves.includes(moveName)) return value;
+ if (itemName === moveType + ' Gem') {
+ value.itemModify(this.battle.gen < 6 ? 1.5 : 1.3);
+ return value;
+ }
+
+ return value;
+ }
+ getPokemonTypes(pokemon: Pokemon | ServerPokemon): ReadonlyArray {
+ if (!(pokemon as Pokemon).getTypes) {
+ return this.battle.dex.getTemplate(pokemon.species).types;
+ }
+
+ return (pokemon as Pokemon).getTypeList();
+ }
+ pokemonHasType(pokemon: Pokemon | ServerPokemon, type: TypeName, types?: ReadonlyArray) {
+ if (!types) types = this.getPokemonTypes(pokemon);
+ for (const curType of types) {
+ if (curType === type) return true;
+ }
+ return false;
+ }
+}
diff --git a/src/battle.ts b/src/battle.ts
index 200e74f58..63f0362b1 100644
--- a/src/battle.ts
+++ b/src/battle.ts
@@ -34,7 +34,7 @@ type WeatherState = [string, number, number];
type EffectTable = {[effectid: string]: EffectState};
type HPColor = 'r' | 'y' | 'g';
-class Pokemon {
+class Pokemon implements PokemonDetails, PokemonHealth {
name = '';
species = '';
@@ -94,7 +94,6 @@ class Pokemon {
volatiles: EffectTable = {};
turnstatuses: EffectTable = {};
movestatuses: EffectTable = {};
- weightkg = 0;
lastMove = '';
/** [[moveName, ppUsed]] */
@@ -132,7 +131,7 @@ class Pokemon {
}
return '';
}
- getPixelRange(pixels: number, color: HPColor): [number, number] {
+ static getPixelRange(pixels: number, color: HPColor | ''): [number, number] {
let epsilon = 0.5 / 714;
if (pixels === 0) return [0, 0];
@@ -155,7 +154,7 @@ class Pokemon {
return [pixels / 48, (pixels + 1) / 48 - epsilon];
}
- getFormattedRange(range: [number, number], precision: number, separator: string) {
+ static getFormattedRange(range: [number, number], precision: number, separator: string) {
if (range[0] === range[1]) {
let percentage = Math.abs(range[0] * 100);
if (Math.floor(percentage) === percentage) {
@@ -185,8 +184,8 @@ class Pokemon {
return [damage[2] / 100, damage[2] / 100];
}
// pixel damage
- let oldrange = this.getPixelRange(damage[3], damage[4]);
- let newrange = this.getPixelRange(damage[3] + damage[0], this.hpcolor);
+ let oldrange = Pokemon.getPixelRange(damage[3], damage[4]);
+ let newrange = Pokemon.getPixelRange(damage[3] + damage[0], this.hpcolor);
if (damage[0] === 0) {
// no change in displayed pixel width
return [0, newrange[1] - newrange[0]];
@@ -384,6 +383,10 @@ class Pokemon {
// let badBoostTable = ['Normal', '−1', '−2', '−3', '−4', '−5', '−6'];
return '' + badBoostTable[-this.boosts[boostStat]] + ' ' + boostStatTable[boostStat];
}
+ getWeightKg(serverPokemon?: ServerPokemon) {
+ let autotomizeFactor = this.volatiles.autotomize ? this.volatiles.autotomize[1] * 100 : 0;
+ return Math.max(this.getTemplate(serverPokemon).weightkg - autotomizeFactor, 0.1);
+ }
getBoostType(boostStat: BoostStatName) {
if (!this.boosts[boostStat]) return 'neutral';
if (this.boosts[boostStat] > 0) return 'good';
@@ -391,9 +394,6 @@ class Pokemon {
}
clearVolatile() {
this.ability = this.baseAbility;
- if (window.BattlePokedex && BattlePokedex[this.species] && BattlePokedex[this.species].weightkg) {
- this.weightkg = BattlePokedex[this.species].weightkg;
- }
this.boosts = {};
this.clearVolatiles();
for (let i = 0; i < this.moveTrack.length; i++) {
@@ -452,29 +452,59 @@ class Pokemon {
this.removeVolatile('typeadd' as ID);
}
}
- getTypes(): [string[], string] {
- let types;
+ getTypes(serverPokemon?: ServerPokemon): [ReadonlyArray, TypeName | ''] {
+ let types: ReadonlyArray;
if (this.volatiles.typechange) {
types = this.volatiles.typechange[1].split('/');
} else {
- const species = this.getSpecies();
- types = (
- window.BattleTeambuilderTable &&
- window.BattleTeambuilderTable['gen' + this.side.battle.gen] &&
- window.BattleTeambuilderTable['gen' + this.side.battle.gen].overrideType[toId(species)]
- );
- if (types) types = types.split('/');
- if (!types) types = Dex.getTemplate(species).types || [];
+ types = this.getTemplate(serverPokemon).types;
+ }
+ if (this.volatiles.roost && types.includes('Flying')) {
+ types = types.filter(typeName => typeName !== 'Flying');
+ if (!types.length) types = ['Normal'];
}
const addedType = (this.volatiles.typeadd ? this.volatiles.typeadd[1] : '');
return [types, addedType];
}
- getTypeList() {
- const [types, addedType] = this.getTypes();
- return types.concat(addedType);
+ isGrounded(serverPokemon?: ServerPokemon) {
+ const battle = this.side.battle;
+ if (battle.hasPseudoWeather('Gravity')) {
+ return true;
+ } else if (this.volatiles['ingrain'] && battle.gen >= 4) {
+ return true;
+ } else if (this.volatiles['smackdown']) {
+ return true;
+ }
+
+ let item = toId(serverPokemon ? serverPokemon.item : this.item);
+ let ability = toId(this.ability || (serverPokemon && serverPokemon.ability));
+ if (battle.hasPseudoWeather('Magic Room') || this.volatiles['embargo'] || ability === 'klutz') {
+ item = '' as ID;
+ }
+
+ if (item === 'ironball') {
+ return true;
+ }
+ if (ability === 'levitate') {
+ return false;
+ }
+ if (this.volatiles['magnetrise'] || this.volatiles['telekinesis']) {
+ return false;
+ } else if (item !== 'airballoon') {
+ return false;
+ }
+ return !this.getTypeList(serverPokemon).includes('Flying');
}
- getSpecies(): string {
- return this.volatiles.formechange ? this.volatiles.formechange[1] : this.species;
+ getTypeList(serverPokemon?: ServerPokemon) {
+ const [types, addedType] = this.getTypes(serverPokemon);
+ return addedType ? types.concat(addedType) : types;
+ }
+ getSpecies(serverPokemon?: ServerPokemon): string {
+ return this.volatiles.formechange ? this.volatiles.formechange[1] :
+ (serverPokemon ? serverPokemon.species : this.species);
+ }
+ getTemplate(serverPokemon?: ServerPokemon) {
+ return this.side.battle.dex.getTemplate(this.getSpecies(serverPokemon));
}
reset() {
this.clearVolatile();
@@ -500,7 +530,7 @@ class Pokemon {
// Draw the health bar to the middle of the range.
// This affects the width of the visual health bar *only*; it
// does not affect the ranges displayed in any way.
- let range = this.getPixelRange(this.hp, this.hpcolor);
+ let range = Pokemon.getPixelRange(this.hp, this.hpcolor);
let ratio = (range[0] + range[1]) / 2;
return Math.round(maxWidth * ratio) || 1;
}
@@ -510,11 +540,11 @@ class Pokemon {
}
return percentage * maxWidth / 100;
}
- hpDisplay(precision = 1) {
- if (this.maxhp === 100) return this.hp + '%';
- if (this.maxhp !== 48) return (100 * this.hp / this.maxhp).toFixed(precision) + '%';
- let range = this.getPixelRange(this.hp, this.hpcolor);
- return this.getFormattedRange(range, precision, '–');
+ static getHPText(pokemon: PokemonHealth, precision = 1) {
+ if (pokemon.maxhp === 100) return pokemon.hp + '%';
+ if (pokemon.maxhp !== 48) return (100 * pokemon.hp / pokemon.maxhp).toFixed(precision) + '%';
+ let range = Pokemon.getPixelRange(pokemon.hp, pokemon.hpcolor);
+ return Pokemon.getFormattedRange(range, precision, '–');
}
destroy() {
if (this.sprite) this.sprite.destroy();
@@ -875,6 +905,48 @@ enum Playback {
Seeking = 5,
}
+interface PokemonDetails {
+ details: string;
+ name: string;
+ species: string;
+ level: number;
+ shiny: boolean;
+ gender: GenderName | '';
+ ident: string;
+ searchid: string;
+}
+interface PokemonHealth {
+ hp: number;
+ maxhp: number;
+ hpcolor: HPColor | '';
+ status: StatusName | 'tox' | '' | '???';
+ fainted?: boolean;
+}
+interface ServerPokemon extends PokemonDetails, PokemonHealth {
+ ident: string;
+ details: string;
+ condition: string;
+ active: boolean;
+ /** unboosted stats */
+ stats: {
+ atk: number,
+ def: number,
+ spa: number,
+ spd: number,
+ spe: number,
+ };
+ /** currently an ID, will revise to name */
+ moves: string[];
+ /** currently an ID, will revise to name */
+ baseAbility: string;
+ /** currently an ID, will revise to name */
+ ability?: string;
+ /** currently an ID, will revise to name */
+ item: string;
+ /** currently an ID, will revise to name */
+ pokeball: string;
+}
+
class Battle {
scene: BattleScene | BattleSceneStub;
@@ -923,9 +995,12 @@ class Battle {
yourSide: Side = null!;
p1: Side = null!;
p2: Side = null!;
+ myPokemon: ServerPokemon[] | null = null;
sides: [Side, Side] = [null!, null!];
lastMove = '';
+
gen = 7;
+ dex: ModdedDex = Dex;
teamPreviewCount = 0;
speciesClause = false;
tier = '';
@@ -934,6 +1009,11 @@ class Battle {
endLastTurnPending = false;
totalTimeLeft = 0;
graceTimeLeft = 0;
+ /**
+ * true: timer on, state unknown
+ * false: timer off
+ * number: seconds left this turn
+ */
kickingInactive: number | boolean = false;
// options
@@ -1394,7 +1474,7 @@ class Battle {
break;
}
} else {
- let damageinfo = '' + poke.getFormattedRange(range, damage[1] === 100 ? 0 : 1, '\u2013');
+ let damageinfo = '' + Pokemon.getFormattedRange(range, damage[1] === 100 ? 0 : 1, '\u2013');
if (damage[1] !== 100) {
let hover = '' + ((damage[0] < 0) ? '\u2212' : '') +
Math.abs(damage[0]) + '/' + damage[1];
@@ -1406,7 +1486,7 @@ class Battle {
}
args[3] = damageinfo;
}
- this.scene.damageAnim(poke, poke.getFormattedRange(range, 0, ' to '));
+ this.scene.damageAnim(poke, Pokemon.getFormattedRange(range, 0, ' to '));
this.log(args, kwArgs);
break;
}
@@ -1439,7 +1519,7 @@ class Battle {
}
}
this.scene.runOtherAnim('heal' as ID, [poke]);
- this.scene.healAnim(poke, poke.getFormattedRange(range, 0, ' to '));
+ this.scene.healAnim(poke, Pokemon.getFormattedRange(range, 0, ' to '));
this.log(args, kwArgs);
break;
}
@@ -1449,7 +1529,7 @@ class Battle {
if (cpoke) {
let damage = cpoke.healthParse(args[2 + 2 * k])!;
let range = cpoke.getDamageRange(damage);
- let formattedRange = cpoke.getFormattedRange(range, 0, ' to ');
+ let formattedRange = Pokemon.getFormattedRange(range, 0, ' to ');
let diff = damage[0];
if (diff > 0) {
this.scene.healAnim(cpoke, formattedRange);
@@ -2059,11 +2139,10 @@ class Battle {
}
newSpecies = args[2].substr(0, commaIndex);
}
- let template = Dex.getTemplate(newSpecies);
+ let template = this.dex.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];
@@ -2084,7 +2163,6 @@ class Battle {
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;
@@ -2188,6 +2266,7 @@ class Battle {
break;
case 'imprison':
this.scene.resultAnim(poke, 'Imprisoning', 'good');
+ break;
case 'disable':
this.scene.resultAnim(poke, 'Disabled', 'bad');
break;
@@ -2239,6 +2318,11 @@ class Battle {
break;
case 'autotomize':
this.scene.resultAnim(poke, 'Lightened', 'good');
+ if (poke.volatiles.autotomize) {
+ poke.volatiles.autotomize[1]++;
+ } else {
+ poke.addVolatile('autotomize' as ID, 1);
+ }
break;
case 'focusenergy':
this.scene.resultAnim(poke, '+Crit rate', 'good');
@@ -2685,7 +2769,7 @@ class Battle {
return data.spriteData[siden];
}
*/
- parseDetails(name: string, pokemonid: string, details = "", output: any = {}) {
+ parseDetails(name: string, pokemonid: string, details = "", output: PokemonDetails = {} as any) {
output.details = details;
output.name = name;
output.species = name;
@@ -2700,7 +2784,7 @@ class Battle {
splitDetails.pop();
}
if (splitDetails[splitDetails.length - 1] === 'M' || splitDetails[splitDetails.length - 1] === 'F') {
- output.gender = splitDetails[splitDetails.length - 1];
+ output.gender = splitDetails[splitDetails.length - 1] as GenderName;
splitDetails.pop();
}
if (splitDetails[1]) {
@@ -2711,13 +2795,7 @@ class Battle {
}
return output;
}
- parseHealth(hpstring: string, output: any = {}): {
- hp: number,
- maxhp: number,
- hpcolor: HPColor | '',
- status: StatusName | '',
- fainted?: boolean,
- } | null {
+ parseHealth(hpstring: string, output: PokemonHealth = {} as any) {
let [hp, status] = hpstring.split(' ');
// hp parse
@@ -3177,6 +3255,7 @@ class Battle {
}
case 'gen': {
this.gen = parseInt(args[1], 10);
+ this.dex = Dex.mod(`gen${this.gen}` as ID);
this.scene.updateGen();
this.log(args);
break;
diff --git a/style/battle.css b/style/battle.css
index 04a437790..95fa45d5f 100644
--- a/style/battle.css
+++ b/style/battle.css
@@ -711,6 +711,60 @@ License: GPLv2
-ms-interpolation-mode: nearest-neighbor;
}
+/*********************************************************
+ * Tooltips
+ *********************************************************/
+
+#tooltipwrapper {
+ position: absolute;
+ top: 400px;
+ left: 100px;
+ text-align: left;
+ color: black;
+ pointer-events: none;
+}
+#tooltipwrapper .tooltipinner {
+ position: relative;
+}
+#tooltipwrapper .tooltip {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 300px;
+ border: 1px solid #888888;
+ background: #EEEEEE;
+ background: rgba(240,240,240,.9);
+ border-radius: 5px;
+ z-index: 50;
+}
+#tooltipwrapper .tooltip h2 {
+ padding: 2px 4px;
+ margin: 0;
+ border-bottom: 1px solid #888888;
+ font-size: 10pt;
+}
+#tooltipwrapper .tooltip h2 small {
+ font-weight: normal;
+}
+#tooltipwrapper .tooltip p {
+ padding: 2px 4px;
+ margin: 0;
+ font-size: 9pt;
+}
+#tooltipwrapper .tooltip p small {
+ font-size: 8pt;
+}
+#tooltipwrapper .tooltip p.section {
+ border-top: 1px solid #888888;
+}
+#tooltipwrapper.tooltip-locked {
+ pointer-events: auto;
+}
+#tooltipwrapper.tooltip-locked .tooltip {
+ border: 2px solid #888888;
+ background: #DEDEDE;
+}
+
/*********************************************************
* Message log styling
*********************************************************/
@@ -829,7 +883,7 @@ License: GPLv2
color: #DDD;
}
.stat-boosted {
- color: #119911;
+ color: #117911;
}
.stat-lowered {
color: #991111;
diff --git a/style/client.css b/style/client.css
index f26a0d0af..c83bf5ff5 100644
--- a/style/client.css
+++ b/style/client.css
@@ -1970,12 +1970,12 @@ a.ilink.yours {
.movemenu button small.type {
padding-top: 3px;
float: left;
- font-size: 7pt;
+ font-size: 8pt;
}
.movemenu button small.pp {
- padding-top: 3px;
+ padding-top: 2px;
float: right;
- font-size: 7pt;
+ font-size: 8pt;
}
.megaevo {
clear: both;
@@ -2135,48 +2135,6 @@ a.ilink.yours {
}
}
-/****************/
-
-#tooltipwrapper {
- position: absolute;
- top: 400px;
- left: 100px;
- text-align: left;
- color: black;
- pointer-events: none;
-}
-#tooltipwrapper .tooltipinner {
- position: relative;
-}
-#tooltipwrapper .tooltip {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 300px;
- border: 1px solid #888888;
- background: #EEEEEE;
- background: rgba(240,240,240,.9);
- border-radius: 5px;
- z-index: 50;
-}
-#tooltipwrapper .tooltip h2 {
- padding: 2px 4px;
- margin: 0;
- border-bottom: 1px solid #888888;
- font-size: 10pt;
-}
-#tooltipwrapper .tooltip h2 small {
- font-weight: normal;
-}
-#tooltipwrapper .tooltip p {
- padding: 2px 4px;
- margin: 0;
- font-size: 8pt;
-}
-#tooltipwrapper .tooltip p.section {
- border-top: 1px solid #888888;
-}
-
/*********************************************************
* Teambuilder
*********************************************************/
diff --git a/testclient.html b/testclient.html
index 91ca8881b..62b580f62 100644
--- a/testclient.html
+++ b/testclient.html
@@ -104,7 +104,7 @@
-
+
diff --git a/tsconfig.json b/tsconfig.json
index 728957bb3..8b9edd422 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,7 +7,7 @@
"jsx": "preserve",
"strict": true
},
- "types": ["node"],
+ "types": [],
"include": [
"./js/lib/preact.d.ts",
"./src/*"
diff --git a/tslint.json b/tslint.json
index 9935a1522..34464fc9b 100644
--- a/tslint.json
+++ b/tslint.json
@@ -32,8 +32,9 @@
"no-bitwise": false,
"prefer-conditional-expression": false,
"no-shadowed-variable": [true, {"temporalDeadZone": false}],
+ "no-switch-case-fall-through": true,
"object-literal-sort-keys": false,
- "object-literal-key-quotes": [true, "as-needed"],
+ "object-literal-key-quotes": false,
"trailing-comma": [
true,
{