From b88d3c946ca70a0369ed85cc58b3421cb00c3a83 Mon Sep 17 00:00:00 2001 From: Hisuian Zoroark <96159984+HisuianZoroark@users.noreply.github.com> Date: Wed, 10 Jan 2024 18:23:11 -0500 Subject: [PATCH] Add HiZo --- data/mods/gen9ssb/abilities.ts | 29 ++ data/mods/gen9ssb/conditions.ts | 26 ++ data/mods/gen9ssb/moves.ts | 41 ++- data/mods/gen9ssb/random-teams.ts | 8 +- data/mods/gen9ssb/scripts.ts | 559 ++++++++++++++++++++++++++++++ sim/global-types.ts | 2 + 6 files changed, 663 insertions(+), 2 deletions(-) diff --git a/data/mods/gen9ssb/abilities.ts b/data/mods/gen9ssb/abilities.ts index cfcdda69f..995f080ff 100644 --- a/data/mods/gen9ssb/abilities.ts +++ b/data/mods/gen9ssb/abilities.ts @@ -330,6 +330,35 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { flags: {}, }, + // HiZo + martyrcomplex: { + shortDesc: "Immune to status and is considered to be asleep. 30% chance to disable when hit.", + name: "Martyr Complex", + onDamagingHitOrder: 1, + onDamagingHit(damage, target, source, move) { + if (!target.hp) { + this.add('-activate', target, 'ability: Martyr Complex'); + this.add('-message', `${target.name} will be avenged!`); + target.side.addSlotCondition(target, 'martyrcomplex'); + } + }, + condition: { + onSwap(target) { + const boosts: SparseBoostsTable = {}; + boosts['spe'] = 1; + if (target.getStat('atk', false, true) > target.getStat('spa', false, true)) { + boosts['atk'] = 1; + } else { + boosts['spa'] = 1; + } + this.boost(boosts, target, target, this.effect); + target.side.removeSlotCondition(target, 'martyrcomplex'); + }, + }, + // Permanent sleep "status" implemented in the relevant sleep-checking effects + flags: {}, + }, + // HoeenHero misspelled: { shortDesc: "SpA 1.5x, Accuracy 0.8x, Never misses, only misspells moves.", diff --git a/data/mods/gen9ssb/conditions.ts b/data/mods/gen9ssb/conditions.ts index 1ec552eb5..cd7aec3ca 100644 --- a/data/mods/gen9ssb/conditions.ts +++ b/data/mods/gen9ssb/conditions.ts @@ -235,6 +235,32 @@ export const Conditions: {[k: string]: ModdedConditionData & {innateName?: strin this.add(`c:|${getName('havi')}|the nightmare swirls and churns unending n_n`); }, }, + hizo: { + noCopy: true, + onStart() { + // TODO: Confirm nicks later + let friends; + const tier = this.sample(['pic', 'sketch', 'ggsp']); + switch (tier) { + case 'pic': + friends = ['chromate', 'yuki', 'YoBuddyTheBaker', 'zoe', 'jasprose']; + break; + case 'sketch': + friends = ['Eggs', 'career ended', 'ponchlake']; + break; + default: + friends = ['roonie217', 'chromate', 'tkhanh', 'lilyhii']; + break; + } + this.add(`c:|${getName('HiZo')}|/pm ${this.sample(friends)}, ${tier}?`); + }, + onSwitchOut() { + this.add(`c:|${getName('HiZo')}|maybe later then`); + }, + onFaint() { + this.add(`c:|${getName('HiZo')}|can i try that matchup again?`); + }, + }, hoeenhero: { noCopy: true, onStart() { diff --git a/data/mods/gen9ssb/moves.ts b/data/mods/gen9ssb/moves.ts index b4dc22cbf..d7665cd78 100644 --- a/data/mods/gen9ssb/moves.ts +++ b/data/mods/gen9ssb/moves.ts @@ -343,7 +343,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { superegoinflation: { accuracy: true, basePower: 0, - category: "Special", + category: "Status", shortDesc: "User heals 25% HP; Target +2 Atk/SpA + Taunt.", name: "Super Ego Inflation", gen: 9, @@ -513,6 +513,45 @@ export const Moves: {[k: string]: ModdedMoveData} = { type: "Ghost", }, + // HiZo + scapegoat: { + accuracy: true, + basePower: 0, + category: "Status", + shortDesc: "User heals 25% HP; Target +2 Atk/SpA + Taunt.", + name: "Scapegoat", + gen: 9, + pp: 5, + priority: 0, + flags: {}, + onTryHit(source) { + if (!this.canSwitch(source.side)) { + this.add('-message', `You have noone to blame but yourself.`); + this.faint(source); + return this.NOT_FAIL; + } + }, + onTryMove() { + this.attrLastMove('[still]'); + }, + onPrepareHit(target, source) { + this.add('-anim', source, 'Swords Dance', source); + }, + onHit(target, source) { + this.add('message', `A decision must be made.`); + }, + slotCondition: 'scapegoat', + // fake switch a la revival blessing + selfSwitch: true, + condition: { + duration: 1, + // reviving implemented in side.ts, kind of + }, + secondary: null, + target: "self", + type: "Dark", + }, + // HoeenHero reprogram: { accuracy: 100, diff --git a/data/mods/gen9ssb/random-teams.ts b/data/mods/gen9ssb/random-teams.ts index 1ff5fa35c..ccb92e795 100644 --- a/data/mods/gen9ssb/random-teams.ts +++ b/data/mods/gen9ssb/random-teams.ts @@ -139,6 +139,12 @@ export const ssbSets: SSBSets = { signatureMove: 'Augur of Ebrietas', evs: {spa: 252, spd: 4, spe: 252}, nature: 'Timid', teraType: 'Ghost', }, + HiZo: { + species: 'Zoroark-Hisui', ability: 'Martyr Complex', item: 'Heavy-Duty Boots', gender: 'M', + moves: ['Last Respects', 'Revival Blessing', 'Spirit Break'], + signatureMove: 'Scapegoat', + evs: {atk: 252, spa: 4, spe: 252}, nature: 'Naive', teraType: 'Fairy', + }, HoeenHero: { species: 'Ludicolo', ability: 'Misspelled', item: 'Life Orb', gender: 'M', moves: [['Hydro Pump', 'Surf'], 'Giga Drain', 'Ice Beam'], @@ -390,7 +396,7 @@ export class RandomStaffBrosTeams extends RandomTeams { this.enforceNoDirectCustomBanlistChanges(); const team: PokemonSet[] = []; - const debug: string[] = []; // Set this to a list of SSB sets to override the normal pool for debugging. + const debug: string[] = ['HiZo']; // Set this to a list of SSB sets to override the normal pool for debugging. const ruleTable = this.dex.formats.getRuleTable(this.format); const monotype = ruleTable.has('sametypeclause') ? this.sample([...this.dex.types.names()]) : false; diff --git a/data/mods/gen9ssb/scripts.ts b/data/mods/gen9ssb/scripts.ts index 0032eecb1..3bc24c46d 100644 --- a/data/mods/gen9ssb/scripts.ts +++ b/data/mods/gen9ssb/scripts.ts @@ -1,4 +1,5 @@ import {SSBSet} from "./random-teams"; +import {ChosenAction} from '../../../sim/side'; import {FS} from '../../../lib'; import {toID} from '../../../sim/dex-data'; @@ -160,6 +161,317 @@ export const Scripts: ModdedBattleScriptsData = { if (move.id === 'wonderwing') return false; return !!move.flags['contact']; }, + // Fake switch needed for HiZo's Scapegoat + runAction(action) { + const pokemonOriginalHP = action.pokemon?.hp; + let residualPokemon: (readonly [Pokemon, number])[] = []; + // returns whether or not we ended in a callback + switch (action.choice) { + case 'start': { + for (const side of this.sides) { + if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length; + } + + this.add('start'); + + // Change Zacian/Zamazenta into their Crowned formes + for (const pokemon of this.getAllPokemon()) { + let rawSpecies: Species | null = null; + if (pokemon.species.id === 'zacian' && pokemon.item === 'rustedsword') { + rawSpecies = this.dex.species.get('Zacian-Crowned'); + } else if (pokemon.species.id === 'zamazenta' && pokemon.item === 'rustedshield') { + rawSpecies = this.dex.species.get('Zamazenta-Crowned'); + } + if (!rawSpecies) continue; + const species = pokemon.setSpecies(rawSpecies); + if (!species) continue; + pokemon.baseSpecies = rawSpecies; + pokemon.details = species.name + (pokemon.level === 100 ? '' : ', L' + pokemon.level) + + (pokemon.gender === '' ? '' : ', ' + pokemon.gender) + (pokemon.set.shiny ? ', shiny' : ''); + pokemon.setAbility(species.abilities['0'], null, true); + pokemon.baseAbility = pokemon.ability; + + const behemothMove: {[k: string]: string} = { + 'Zacian-Crowned': 'behemothblade', 'Zamazenta-Crowned': 'behemothbash', + }; + const ironHead = pokemon.baseMoves.indexOf('ironhead'); + if (ironHead >= 0) { + const move = this.dex.moves.get(behemothMove[rawSpecies.name]); + pokemon.baseMoveSlots[ironHead] = { + move: move.name, + id: move.id, + pp: (move.noPPBoosts || move.isZ) ? move.pp : move.pp * 8 / 5, + maxpp: (move.noPPBoosts || move.isZ) ? move.pp : move.pp * 8 / 5, + target: move.target, + disabled: false, + disabledSource: '', + used: false, + }; + pokemon.moveSlots = pokemon.baseMoveSlots.slice(); + } + } + + if (this.format.onBattleStart) this.format.onBattleStart.call(this); + for (const rule of this.ruleTable.keys()) { + if ('+*-!'.includes(rule.charAt(0))) continue; + const subFormat = this.dex.formats.get(rule); + if (subFormat.onBattleStart) subFormat.onBattleStart.call(this); + } + + for (const side of this.sides) { + for (let i = 0; i < side.active.length; i++) { + if (!side.pokemonLeft) { + // forfeited before starting + side.active[i] = side.pokemon[i]; + side.active[i].fainted = true; + side.active[i].hp = 0; + } else { + this.actions.switchIn(side.pokemon[i], i); + } + } + } + for (const pokemon of this.getAllPokemon()) { + this.singleEvent('Start', this.dex.conditions.getByID(pokemon.species.id), pokemon.speciesState, pokemon); + } + this.midTurn = true; + break; + } + + case 'move': + if (!action.pokemon.isActive) return false; + if (action.pokemon.fainted) return false; + this.actions.runMove(action.move, action.pokemon, action.targetLoc, action.sourceEffect, + action.zmove, undefined, action.maxMove, action.originalTarget); + break; + case 'megaEvo': + this.actions.runMegaEvo(action.pokemon); + break; + case 'runDynamax': + action.pokemon.addVolatile('dynamax'); + action.pokemon.side.dynamaxUsed = true; + if (action.pokemon.side.allySide) action.pokemon.side.allySide.dynamaxUsed = true; + break; + case 'terastallize': + this.actions.terastallize(action.pokemon); + break; + case 'beforeTurnMove': + if (!action.pokemon.isActive) return false; + if (action.pokemon.fainted) return false; + this.debug('before turn callback: ' + action.move.id); + const target = this.getTarget(action.pokemon, action.move, action.targetLoc); + if (!target) return false; + if (!action.move.beforeTurnCallback) throw new Error(`beforeTurnMove has no beforeTurnCallback`); + action.move.beforeTurnCallback.call(this, action.pokemon, target); + break; + case 'priorityChargeMove': + if (!action.pokemon.isActive) return false; + if (action.pokemon.fainted) return false; + this.debug('priority charge callback: ' + action.move.id); + if (!action.move.priorityChargeCallback) throw new Error(`priorityChargeMove has no priorityChargeCallback`); + action.move.priorityChargeCallback.call(this, action.pokemon); + break; + + case 'event': + this.runEvent(action.event!, action.pokemon); + break; + case 'team': + if (action.index === 0) { + action.pokemon.side.pokemon = []; + } + action.pokemon.side.pokemon.push(action.pokemon); + action.pokemon.position = action.index; + // we return here because the update event would crash since there are no active pokemon yet + return; + + case 'pass': + return; + case 'instaswitch': + case 'switch': + if (action.choice === 'switch' && action.pokemon.status) { + this.singleEvent('CheckShow', this.dex.abilities.getByID('naturalcure' as ID), null, action.pokemon); + } + if (this.actions.switchIn(action.target, action.pokemon.position, action.sourceEffect) === 'pursuitfaint') { + // a pokemon fainted from Pursuit before it could switch + if (this.gen <= 4) { + // in gen 2-4, the switch still happens + this.hint("Previously chosen switches continue in Gen 2-4 after a Pursuit target faints."); + action.priority = -101; + this.queue.unshift(action); + break; + } else { + // in gen 5+, the switch is cancelled + this.hint("A Pokemon can't switch between when it runs out of HP and when it faints"); + break; + } + } + break; + case 'revivalblessing': + action.pokemon.side.pokemonLeft++; + if (action.target.position < action.pokemon.side.active.length) { + this.queue.addChoice({ + choice: 'instaswitch', + pokemon: action.target, + target: action.target, + }); + } + action.target.fainted = false; + action.target.faintQueued = false; + action.target.subFainted = false; + action.target.status = ''; + action.target.hp = 1; // Needed so hp functions works + action.target.sethp(action.target.maxhp / 2); + this.add('-heal', action.target, action.target.getHealth, '[from] move: Revival Blessing'); + action.pokemon.side.removeSlotCondition(action.pokemon, 'revivalblessing'); + break; + // @ts-ignore I'm sorry but it takes a lot + case 'scapegoat': + // Clientside error messages if mon is unrevealed, should be supported. + this.add('-message', `It is written.`); + // @ts-ignore + if (action.target.faint()) { + // @ts-ignore + this.boost({atk: 2, spa: 2, spe: 2}, action.pokemon, action.pokemon, this.dex.moves.get('scapegoat')); + } + // @ts-ignore + action.pokemon.side.removeSlotCondition(action.pokemon, 'scapegoat'); + break; + case 'runUnnerve': + this.singleEvent('PreStart', action.pokemon.getAbility(), action.pokemon.abilityState, action.pokemon); + break; + case 'runSwitch': + this.actions.runSwitch(action.pokemon); + break; + case 'runPrimal': + if (!action.pokemon.transformed) { + this.singleEvent('Primal', action.pokemon.getItem(), action.pokemon.itemState, action.pokemon); + } + break; + case 'shift': + if (!action.pokemon.isActive) return false; + if (action.pokemon.fainted) return false; + this.swapPosition(action.pokemon, 1); + break; + + case 'beforeTurn': + this.eachEvent('BeforeTurn'); + break; + case 'residual': + this.add(''); + this.clearActiveMove(true); + this.updateSpeed(); + residualPokemon = this.getAllActive().map(pokemon => [pokemon, pokemon.getUndynamaxedHP()] as const); + this.residualEvent('Residual'); + this.add('upkeep'); + break; + } + + // phazing (Roar, etc) + for (const side of this.sides) { + for (const pokemon of side.active) { + if (pokemon.forceSwitchFlag) { + if (pokemon.hp) this.actions.dragIn(pokemon.side, pokemon.position); + pokemon.forceSwitchFlag = false; + } + } + } + + this.clearActiveMove(); + + // fainting + + this.faintMessages(); + if (this.ended) return true; + + // switching (fainted pokemon, U-turn, Baton Pass, etc) + + if (!this.queue.peek() || (this.gen <= 3 && ['move', 'residual'].includes(this.queue.peek()!.choice))) { + // in gen 3 or earlier, switching in fainted pokemon is done after + // every move, rather than only at the end of the turn. + this.checkFainted(); + } else if (action.choice === 'megaEvo' && this.gen === 7) { + this.eachEvent('Update'); + // In Gen 7, the action order is recalculated for a Pokémon that mega evolves. + for (const [i, queuedAction] of this.queue.list.entries()) { + if (queuedAction.pokemon === action.pokemon && queuedAction.choice === 'move') { + this.queue.list.splice(i, 1); + queuedAction.mega = 'done'; + this.queue.insertChoice(queuedAction, true); + break; + } + } + return false; + } else if (this.queue.peek()?.choice === 'instaswitch') { + return false; + } + + if (this.gen >= 5) { + this.eachEvent('Update'); + for (const [pokemon, originalHP] of residualPokemon) { + const maxhp = pokemon.getUndynamaxedHP(pokemon.maxhp); + if (pokemon.hp && pokemon.getUndynamaxedHP() <= maxhp / 2 && originalHP > maxhp / 2) { + this.runEvent('EmergencyExit', pokemon); + } + } + } + + if (action.choice === 'runSwitch') { + const pokemon = action.pokemon; + if (pokemon.hp && pokemon.hp <= pokemon.maxhp / 2 && pokemonOriginalHP! > pokemon.maxhp / 2) { + this.runEvent('EmergencyExit', pokemon); + } + } + + const switches = this.sides.map( + side => side.active.some(pokemon => pokemon && !!pokemon.switchFlag) + ); + + for (let i = 0; i < this.sides.length; i++) { + let reviveSwitch = false; // Used to ignore the fake switch for Revival Blessing + if (switches[i] && !this.canSwitch(this.sides[i])) { + for (const pokemon of this.sides[i].active) { + if (this.sides[i].slotConditions[pokemon.position]['revivalblessing'] || this.sides[i].slotConditions[pokemon.position]['scapegoat']) { + reviveSwitch = true; + continue; + } + pokemon.switchFlag = false; + } + if (!reviveSwitch) switches[i] = false; + } else if (switches[i]) { + for (const pokemon of this.sides[i].active) { + if (pokemon.hp && pokemon.switchFlag && pokemon.switchFlag !== 'revivalblessing' && + !pokemon.skipBeforeSwitchOutEventFlag) { + this.runEvent('BeforeSwitchOut', pokemon); + pokemon.skipBeforeSwitchOutEventFlag = true; + this.faintMessages(); // Pokemon may have fainted in BeforeSwitchOut + if (this.ended) return true; + if (pokemon.fainted) { + switches[i] = this.sides[i].active.some(sidePokemon => sidePokemon && !!sidePokemon.switchFlag); + } + } + } + } + } + + for (const playerSwitch of switches) { + if (playerSwitch) { + this.makeRequest('switch'); + return true; + } + } + + if (this.gen < 5) this.eachEvent('Update'); + + if (this.gen >= 8 && (this.queue.peek()?.choice === 'move' || this.queue.peek()?.choice === 'runDynamax')) { + // In gen 8, speed is updated dynamically so update the queue's speed properties and sort it. + this.updateSpeed(); + for (const queueAction of this.queue.list) { + if (queueAction.pokemon) this.getActionSpeed(queueAction); + } + this.queue.sort(); + } + + return false; + }, actions: { canTerastallize(pokemon) { if ( @@ -657,4 +969,251 @@ export const Scripts: ModdedBattleScriptsData = { return hitResults; }, }, + side: { + getChoice() { + if (this.choice.actions.length > 1 && this.choice.actions.every(action => action.choice === 'team')) { + return `team ` + this.choice.actions.map(action => action.pokemon!.position + 1).join(', '); + } + return this.choice.actions.map(action => { + switch (action.choice) { + case 'move': + let details = ``; + if (action.targetLoc && this.active.length > 1) details += ` ${action.targetLoc > 0 ? '+' : ''}${action.targetLoc}`; + if (action.mega) details += (action.pokemon!.item === 'ultranecroziumz' ? ` ultra` : ` mega`); + if (action.zmove) details += ` zmove`; + if (action.maxMove) details += ` dynamax`; + if (action.terastallize) details += ` terastallize`; + return `move ${action.moveid}${details}`; + case 'switch': + case 'instaswitch': + case 'revivalblessing': + // @ts-ignore custom status + case 'scapegoat': + return `switch ${action.target!.position + 1}`; + case 'team': + return `team ${action.pokemon!.position + 1}`; + default: + return action.choice; + } + }).join(', '); + }, + + chooseSwitch(slotText) { + if (this.requestState !== 'move' && this.requestState !== 'switch') { + return this.emitChoiceError(`Can't switch: You need a ${this.requestState} response`); + } + const index = this.getChoiceIndex(); + if (index >= this.active.length) { + if (this.requestState === 'switch') { + return this.emitChoiceError(`Can't switch: You sent more switches than Pokémon that need to switch`); + } + return this.emitChoiceError(`Can't switch: You sent more choices than unfainted Pokémon`); + } + const pokemon = this.active[index]; + let slot; + if (!slotText) { + if (this.requestState !== 'switch') { + return this.emitChoiceError(`Can't switch: You need to select a Pokémon to switch in`); + } + if (this.slotConditions[pokemon.position]['revivalblessing']) { + slot = 0; + while (!this.pokemon[slot].fainted) slot++; + } else { + if (!this.choice.forcedSwitchesLeft) return this.choosePass(); + slot = this.active.length; + while (this.choice.switchIns.has(slot) || this.pokemon[slot].fainted) slot++; + } + } else { + slot = parseInt(slotText) - 1; + } + if (isNaN(slot) || slot < 0) { + // maybe it's a name/species id! + slot = -1; + for (const [i, mon] of this.pokemon.entries()) { + if (slotText!.toLowerCase() === mon.name.toLowerCase() || toID(slotText) === mon.species.id) { + slot = i; + break; + } + } + if (slot < 0) { + return this.emitChoiceError(`Can't switch: You do not have a Pokémon named "${slotText}" to switch to`); + } + } + if (slot >= this.pokemon.length) { + return this.emitChoiceError(`Can't switch: You do not have a Pokémon in slot ${slot + 1} to switch to`); + } else if (slot < this.active.length && !this.slotConditions[pokemon.position]['revivalblessing']) { + return this.emitChoiceError(`Can't switch: You can't switch to an active Pokémon`); + } else if (this.choice.switchIns.has(slot)) { + return this.emitChoiceError(`Can't switch: The Pokémon in slot ${slot + 1} can only switch in once`); + } + const targetPokemon = this.pokemon[slot]; + + if (this.slotConditions[pokemon.position]['revivalblessing']) { + if (!targetPokemon.fainted) { + return this.emitChoiceError(`Can't switch: You have to pass to a fainted Pokémon`); + } + // Should always subtract, but stop at 0 to prevent errors. + this.choice.forcedSwitchesLeft = this.battle.clampIntRange(this.choice.forcedSwitchesLeft - 1, 0); + pokemon.switchFlag = false; + this.choice.actions.push({ + choice: 'revivalblessing', + pokemon, + target: targetPokemon, + } as ChosenAction); + return true; + } + + if (targetPokemon.fainted) { + return this.emitChoiceError(`Can't switch: You can't switch to a fainted Pokémon`); + } + + if (this.slotConditions[pokemon.position]['scapegoat']) { + // Should always subtract, but stop at 0 to prevent errors. + this.choice.forcedSwitchesLeft = this.battle.clampIntRange(this.choice.forcedSwitchesLeft - 1, 0); + pokemon.switchFlag = false; + // @ts-ignore custom request + this.choice.actions.push({ + choice: 'scapegoat', + pokemon, + target: targetPokemon, + } as ChosenAction); + return true; + } + + if (this.requestState === 'move') { + if (pokemon.trapped) { + const includeRequest = this.updateRequestForPokemon(pokemon, req => { + let updated = false; + if (req.maybeTrapped) { + delete req.maybeTrapped; + updated = true; + } + if (!req.trapped) { + req.trapped = true; + updated = true; + } + return updated; + }); + const status = this.emitChoiceError(`Can't switch: The active Pokémon is trapped`, includeRequest); + if (includeRequest) this.emitRequest(this.activeRequest!); + return status; + } else if (pokemon.maybeTrapped) { + this.choice.cantUndo = this.choice.cantUndo || pokemon.isLastActive(); + } + } else if (this.requestState === 'switch') { + if (!this.choice.forcedSwitchesLeft) { + throw new Error(`Player somehow switched too many Pokemon`); + } + this.choice.forcedSwitchesLeft--; + } + + this.choice.switchIns.add(slot); + + this.choice.actions.push({ + choice: (this.requestState === 'switch' ? 'instaswitch' : 'switch'), + pokemon, + target: targetPokemon, + } as ChosenAction); + + return true; + }, + }, + queue: { + resolveAction(action, midTurn) { + if (!action) throw new Error(`Action not passed to resolveAction`); + if (action.choice === 'pass') return []; + const actions = [action]; + + if (!action.side && action.pokemon) action.side = action.pokemon.side; + if (!action.move && action.moveid) action.move = this.battle.dex.getActiveMove(action.moveid); + if (!action.order) { + const orders: {[choice: string]: number} = { + team: 1, + start: 2, + instaswitch: 3, + beforeTurn: 4, + beforeTurnMove: 5, + revivalblessing: 6, + scapegoat: 7, + + runUnnerve: 100, + runSwitch: 101, + runPrimal: 102, + switch: 103, + megaEvo: 104, + runDynamax: 105, + terastallize: 106, + priorityChargeMove: 107, + + shift: 200, + // default is 200 (for moves) + + residual: 300, + }; + if (action.choice in orders) { + action.order = orders[action.choice]; + } else { + action.order = 200; + if (!['move', 'event'].includes(action.choice)) { + throw new Error(`Unexpected orderless action ${action.choice}`); + } + } + } + if (!midTurn) { + if (action.choice === 'move') { + if (!action.maxMove && !action.zmove && action.move.beforeTurnCallback) { + actions.unshift(...this.resolveAction({ + choice: 'beforeTurnMove', pokemon: action.pokemon, move: action.move, targetLoc: action.targetLoc, + })); + } + if (action.mega && !action.pokemon.isSkyDropped()) { + actions.unshift(...this.resolveAction({ + choice: 'megaEvo', + pokemon: action.pokemon, + })); + } + if (action.terastallize && !action.pokemon.terastallized) { + actions.unshift(...this.resolveAction({ + choice: 'terastallize', + pokemon: action.pokemon, + })); + } + if (action.maxMove && !action.pokemon.volatiles['dynamax']) { + actions.unshift(...this.resolveAction({ + choice: 'runDynamax', + pokemon: action.pokemon, + })); + } + if (!action.maxMove && !action.zmove && action.move.priorityChargeCallback) { + actions.unshift(...this.resolveAction({ + choice: 'priorityChargeMove', + pokemon: action.pokemon, + move: action.move, + })); + } + action.fractionalPriority = this.battle.runEvent('FractionalPriority', action.pokemon, null, action.move, 0); + } else if (['switch', 'instaswitch'].includes(action.choice)) { + if (typeof action.pokemon.switchFlag === 'string') { + action.sourceEffect = this.battle.dex.moves.get(action.pokemon.switchFlag as ID) as any; + } + action.pokemon.switchFlag = false; + } + } + + const deferPriority = this.battle.gen === 7 && action.mega && action.mega !== 'done'; + if (action.move) { + let target = null; + action.move = this.battle.dex.getActiveMove(action.move); + + if (!action.targetLoc) { + target = this.battle.getRandomTarget(action.pokemon, action.move); + // TODO: what actually happens here? + if (target) action.targetLoc = action.pokemon.getLocOf(target); + } + action.originalTarget = action.pokemon.getAtLoc(action.targetLoc); + } + if (!deferPriority) this.battle.getActionSpeed(action); + return actions as any; + }, + }, }; diff --git a/sim/global-types.ts b/sim/global-types.ts index 1f868a51a..7a0b7ed68 100644 --- a/sim/global-types.ts +++ b/sim/global-types.ts @@ -308,6 +308,8 @@ interface ModdedBattleActions { interface ModdedBattleSide { canDynamaxNow?: (this: Side) => boolean; + chooseSwitch?: (this: Side, slotText?: string) => any; + getChoice?: (this: Side) => string; getRequestData?: (this: Side, forAlly?: boolean) => {name: string, id: ID, pokemon: AnyObject[]}; }