Compare commits

...
88 changed files with 2521 additions and 1766 deletions

No files matched your search

@@ -135,28 +135,31 @@ class BattleMessage(rawMessage: String) {
return this.actorAndActivePokemon(pnx, battle)
}
/*
Example:
|-curestatus|p1: a8e5710e-6835-4387-99d3-331b45f848e6|slp|[msg]
|-curestatus|p1: 0686b20d-3019-4167-92e2-786834245743|slp|[msg]
// Function used to grab BattlePokemon (As we need to be able to grab non active pokemon to cure their status)
// by using playerNumber(actorID) and pokemon uuid(pokemonID)
/**
* Queries an argument at the given [index] for a 'pnx' and uuid that will be parsed into a [BattlePokemon].
*
* @param index The index of the argument referencing the [BattlePokemon].
* @param battle The [PokemonBattle] being queried.
* @return The [BattlePokemon] if the argument exists and is successfully parsed; otherwise null.
*/
fun getBattlePokemon(index: Int, battle: PokemonBattle): BattlePokemon? {
val actorAndPokemonID = this.argumentAt(index)?.takeIf { it.length >= 2 }?.split(": ") ?: return null
if (actorAndPokemonID.count() < 2) return null
val actorID = actorAndPokemonID[0]
val pokemonID = actorAndPokemonID[1]
return this.getBattlePokemon(actorID, pokemonID, battle)
fun battlePokemon(index: Int, battle: PokemonBattle): BattlePokemon? {
val (actorID, pokemonID) = this.pnxAndUuid(index) ?: return null
return this.battlePokemon(actorID, pokemonID, battle)
}
fun getSourceBattlePokemon(battle: PokemonBattle): BattlePokemon? {
val sourcePokemonArgument = this.optionalArguments.get("of") ?: return null
val actorAndPokemonID = sourcePokemonArgument.takeIf { it.length >= 2 }?.split(": ") ?: return null
if (actorAndPokemonID.count() < 2) return null
val actorID = actorAndPokemonID[0]
val pokemonID = actorAndPokemonID[1]
return this.getBattlePokemon(actorID, pokemonID, battle)
/**
* Queries an optional argument identified by [optionalArg] for a 'pnx' and uuid that will be parsed into a [BattlePokemon].
*
* @param optionalArg The id of the optional argument referencing the [BattlePokemon].
* @param battle The [PokemonBattle] being queried.
* @return The [BattlePokemon] if the argument exists and is successfully parsed; otherwise null.
*/
fun battlePokemonFromOptional(battle: PokemonBattle, optionalArg: String = "of"): BattlePokemon? {
val optional = this.optionalArguments.get(optionalArg) ?: return null
val pokemonID = optional.takeIf { it.length >= 2 }?.split(":")?.takeIf { it.size == 2 } ?: return null
val pnx = pokemonID[0].takeIf { it.matches(PNX_MATCHER) || it.matches(PN_MATCHER) } ?: return null
val uuid = pokemonID[1].trim()
return this.battlePokemon(pnx, uuid, battle)
}
/**
@@ -234,7 +237,7 @@ class BattleMessage(rawMessage: String) {
null
}
private fun getBattlePokemon(pnx: String, pokemonID: String, battle: PokemonBattle): BattlePokemon? = try {
private fun battlePokemon(pnx: String, pokemonID: String, battle: PokemonBattle): BattlePokemon? = try {
battle.getBattlePokemon(pnx, pokemonID)
} catch (_: Exception) {
null
@@ -8,13 +8,11 @@
package com.cobblemon.mod.common.api.battles.model
import com.bedrockk.molang.runtime.MoLangEnvironment
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.Environment
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
@@ -22,8 +20,6 @@ import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.api.battles.model.actor.FleeableBattleActor
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunction
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.text.red
@@ -44,7 +40,6 @@ import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMusicPacket
import com.cobblemon.mod.common.pokemon.evolution.progress.DefeatEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.requirements.DefeatRequirement
@@ -107,6 +102,11 @@ open class PokemonBattle(
var turn: Int = 1
private set
private var ticks: Int = 0
set(value) { field = value.coerceAtMost(Int.MAX_VALUE) } // go outside
/** The current duration of the battle in seconds. */
val time: Int get() = ticks % 20
var dispatchResult = GO
val dispatches = ConcurrentLinkedDeque<BattleDispatch>()
@@ -166,7 +166,7 @@ open class PokemonBattle(
fun getActor(player: ServerPlayerEntity) = actors.firstOrNull { it.isForPlayer(player) }
/**
* Gets a BattleActor and an [ActiveBattlePokemon] from a pnx key, e.g. p2a
* Gets a [BattleActor] and an [ActiveBattlePokemon] from a pnx key, e.g. p2a
*
* Returns null if either the pn or x is invalid.
*/
@@ -179,6 +179,11 @@ open class PokemonBattle(
return actor to pokemon
}
/**
* Gets a [BattlePokemon] from a pnx key and uuid.
*
* Returns null if the pnx key is invalid or the uuid does not exist.
*/
fun getBattlePokemon(pnx: String, pokemonID: String): BattlePokemon {
val actor = actors.find { it.showdownId == pnx.substring(0, 2) }
?: throw IllegalStateException("Invalid pnx: $pnx - unknown actor")
@@ -390,8 +395,9 @@ open class PokemonBattle(
return
}
if (started && isPvW && !ended && dispatches.isEmpty()) {
checkFlee()
if (started) {
ticks++
if (isPvW && !ended && dispatches.isEmpty()) checkFlee()
}
}
@@ -0,0 +1,89 @@
package com.cobblemon.mod.common.api.entity.pokemon
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.effects.IllusionEffect
import com.cobblemon.mod.common.entity.pokemon.effects.TransformEffect
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import net.minecraft.nbt.NbtCompound
import java.util.concurrent.CompletableFuture
import kotlin.reflect.KClass
/**
* Represents a temporary alteration to how a [PokemonEntity] is rendered and behaves. May include a temporary visual effect
* when applied.
*
* @author Segfault Guy
* @since March 5th, 2024
*/
interface EntityEffect {
/**
* Starts this effect for the provided [PokemonEntity].
*
* @return A [CompletableFuture] that completes after the effect has been applied. Or null if the effect failed to start.
*/
fun start(entity: PokemonEntity): CompletableFuture<PokemonEntity>?
/**
* Ends this effect for the provided [PokemonEntity].
*
* @return A [CompletableFuture] that completes after the effect has been reverted. Or null if the effect failed to end.
*/
fun end(entity: PokemonEntity): CompletableFuture<PokemonEntity>?
/** Saves this effect to NBT. */
fun saveToNbt(): NbtCompound
/** Loads this effect from NBT. */
fun loadFromNBT(nbt: NbtCompound)
companion object {
private val effects = mutableMapOf<String, KClass<out EntityEffect>>()
private val defaults = mutableMapOf<String, () -> EntityEffect>()
init {
register(IllusionEffect.ID, IllusionEffect::class, ::IllusionEffect)
register(TransformEffect.ID, TransformEffect::class, ::TransformEffect)
}
fun <T : EntityEffect> register(id: String, type: KClass<T>, default: () -> T) {
effects[id] = type
defaults[id] = default
}
fun createDefault(id: String): EntityEffect? = defaults[id]?.invoke()
fun loadFromNbt(nbt: NbtCompound): EntityEffect? {
if (nbt.contains(DataKeys.ENTITY_EFFECT_ID)) {
val id = nbt.getString(DataKeys.ENTITY_EFFECT_ID)
return createDefault(id)?.also { it.loadFromNBT(nbt) }
}
return null
}
}
}
/** An [EntityEffect] that modifies the dimensions of a [PokemonEntity]. */
interface PhysicalEffect : EntityEffect {
val scale: Float
}
/** An [EntityEffect] that alters the physical appearance of a [PokemonEntity] to match a [mock]. */
interface MocKEffect : PhysicalEffect {
val mock: PokemonProperties
val exposedSpecies: Species?
get() = this.mock.species?.let { PokemonSpecies.getByIdentifier(it.asIdentifierDefaultingNamespace()) }
val exposedForm: FormData?
get() = this.mock.form?.let {
formID -> this.exposedSpecies?.forms?.firstOrNull { it.formOnlyShowdownId().equals(formID, true) } }
?: this.exposedSpecies?.standardForm
}
@@ -48,6 +48,11 @@ fun interface PokemonPropertyExtractor {
@JvmField
val STATUS = add { pokemon, properties -> properties.status = pokemon.status?.status?.showdownName }
@JvmField
val ILLUSION = mutableListOf(SPECIES, FORM, ASPECTS, GENDER, NICKNAME, SHINY)
@JvmField
val TRANSFORM = mutableListOf(SPECIES, FORM, ASPECTS, GENDER)
fun add(extractor: PokemonPropertyExtractor): PokemonPropertyExtractor {
ALL.add(extractor)
return extractor
@@ -49,6 +49,25 @@ enum class Stats(override val identifier: Identifier, override val displayName:
*/
val BATTLE_ONLY: Set<Stat> = EnumSet.of(EVASION, ACCURACY)
/** Gets the [Stat] from the respective Showdown id. */
fun getStat(statKey: String) = when(statKey) {
"atk" -> ATTACK
"def" -> DEFENCE
"spa" -> SPECIAL_ATTACK
"spd" -> SPECIAL_DEFENCE
"spe" -> SPEED
"evasion" -> EVASION
else -> ACCURACY
}
/** Gets the severity lang key from the respective boost/unboost stage. */
fun getSeverity(stages: Int) = when(stages) {
0 -> "cap.single"
1 -> "slight"
2 -> "sharp"
else -> "severe"
}
}
}
@@ -260,7 +260,7 @@ open class PartyStore(override val uuid: UUID) : PokemonStore<PartyPosition>() {
return@mapNotNull if (clone) {
BattlePokemon.safeCopyOf(it)
} else {
BattlePokemon(it)
BattlePokemon.playerOwned(it)
}
}.sortedBy { if (it.uuid == leadingPokemon) 0 else (indexOf(it.originalPokemon) + 1) }
@@ -17,6 +17,8 @@ class ActiveBattlePokemon(val actor: BattleActor, var battlePokemon: BattlePokem
fun getSide() = actor.getSide()
var position: Pair<ServerWorld, Vec3d>? = null
var illusion: BattlePokemon? = null
override fun getAllActivePokemon() = battle.activePokemon
override fun getActorShowdownId() = actor.showdownId
override fun getActorPokemon() = actor.activePokemon
File diff suppressed because it is too large. Load diff
@@ -19,23 +19,23 @@ import com.cobblemon.mod.common.battles.dispatch.WaitDispatch
import com.cobblemon.mod.common.util.battleLang
/**
* Format:
* |-ability|POKEMON|ABILITY|(from)EFFECT
* Format: |-ability|POKEMON|ABILITY|(from)EFFECT
*
* The ABILITY of the POKEMON has been changed due to a move/ability EFFECT.
* The ABILITY of POKEMON has been changed due to a move/ability EFFECT.
*
* Format:
* |-ability|POKEMON|ABILITY
* Alt format: |-ability|POKEMON|ABILITY
*
* POKEMON has just switched-in, and its ability ABILITY is being announced to have a long-term effect.
* POKEMON has just switched-in, and its ABILITY is being announced to have a long-term effect.
* @author Xylopia
* @since January 31, 2023
*/
class AbilityInstruction(val instructionSet: InstructionSet, val message: BattleMessage) : InterpreterInstruction, CauserInstruction {
override fun invoke(battle: PokemonBattle) {
val pokemon = message.getBattlePokemon(0, battle) ?: return
val pokemon = message.battlePokemon(0, battle) ?: return
val pokemonName = pokemon.getName()
val effect = message.effectAt(1) ?: return
val optionalEffect = message.effect()
val optionalPokemon = message.getSourceBattlePokemon(battle)
val optionalPokemon = message.battlePokemonFromOptional(battle)
val optionalPokemonName = optionalPokemon?.getName()
// If there is an optional effect causing the activation, broadcast that instead of the standard effect
@@ -20,16 +20,19 @@ import com.cobblemon.mod.common.util.battleLang
import net.minecraft.text.Text
/**
* Format:
* |-activate|POKEMON|EFFECT
* Format: |-activate|POKEMON|EFFECT
*
* A miscellaneous effect has activated.This is triggered whenever an effect could not be better described by one of the other minor messages.
* A miscellaneous EFFECT has activated for POKEMON. This is triggered whenever an effect could not be better described
* by one of the other minor messages.
*
* @author Hunter
* @since September 25, 2022
*/
class ActivateInstruction(val instructionSet: InstructionSet, val message: BattleMessage) : InterpreterInstruction, CauserInstruction {
override fun invoke(battle: PokemonBattle) {
val pokemon = message.getBattlePokemon(0, battle) ?: return
val pokemon = message.battlePokemon(0, battle) ?: return
val pokemonName = pokemon.getName()
val sourceName = message.getSourceBattlePokemon(battle)?.getName() ?: Text.literal("UNKNOWN")
val sourceName = message.battlePokemonFromOptional(battle)?.getName() ?: Text.literal("UNKNOWN")
val effect = message.effectAt(1) ?: return
val extraEffect = message.effectAt(2)?.typelessData ?: Text.literal("UNKNOWN")
ShowdownInterpreter.broadcastOptionalAbility(battle, effect, pokemonName)
@@ -0,0 +1,29 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |bagitem|POKEMON|ITEM
*
* POKEMON had ITEM used on it from the 'bag'.
* @author landonjw
* @since July 31, 2023
*/
class BagItemInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
val pokemon = message.pokemonByUuid(0, battle)!!
val item = message.argumentAt(1)!!
val ownerName = pokemon.actor.getName()
val itemName = item.asTranslated()
battle.broadcastChatMessage(battleLang("bagitem.use", ownerName, itemName, pokemon.getName()))
}
}
}
@@ -0,0 +1,30 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-block|POKEMON|EFFECT|MOVE|ATTACKER|(of)SOURCE
*
* An effect targeted at POKEMON was blocked by EFFECT. This may optionally specify that the effect was a MOVE from ATTACKER.
*
* An optional SOURCE will note the owner of the EFFECT, in the case that it's not EFFECT (for instance, an ally with Aroma Veil.)
* @author jeffw773
* @since November 6, 2023
*/
class BlockInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val effectID = message.effectAt(1)?.id ?: return@dispatchWaiting
val lang = battleLang("block.$effectID", pokemonName)
battle.broadcastChatMessage(lang)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,44 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-boost|POKEMON|STAT|AMOUNT or |-unboost|POKEMON|STAT|AMOUNT
*
* POKEMON has gained or lost AMOUNT in STAT, using the standard rules for stat changes in-battle.
* STAT is a standard three-letter abbreviation fot the stat in question.
* @author Hiroku
* @since August 20, 2022
*/
class BoostInstruction(val instructionSet: InstructionSet, val message: BattleMessage, val remainingLines: Iterator<BattleMessage>, val isBoost: Boolean = true): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val pokemon = message.battlePokemon(0, battle) ?: return
val statKey = message.argumentAt(1) ?: return
val stages = message.argumentAt(2)?.toInt() ?: return
val stat = Stats.getStat(statKey).displayName
val severity = Stats.getSeverity(stages)
val rootKey = if (isBoost) "boost" else "unboost"
battle.dispatchWaiting(1.5F) {
val lang = when {
message.hasOptionalArgument("zeffect") -> battleLang("$rootKey.$severity.zeffect", pokemon.getName(), stat)
else -> battleLang("$rootKey.$severity", pokemon.getName(), stat)
}
battle.broadcastChatMessage(lang)
val boostBucket = if (isBoost) BattleContext.Type.BOOST else BattleContext.Type.UNBOOST
val context = ShowdownInterpreter.getContextFromAction(message, boostBucket, battle)
// TODO: replace with context that tracks detailed information such as # of stages
repeat(stages) { pokemon.contextManager.add(context) }
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,43 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.lang
/**
* Formats: |cant|POKEMON|REASON and |cant|POKEMON|REASON|MOVE
*
* The POKEMON could not perform a move because of the indicated REASON.
* @author Deltric
* @since January 22, 2022
*/
class CantInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val effectID = message.effectAt(1)?.id ?: return@dispatchWaiting
val name = pokemon.getName()
// Move may be null as it's not always given
val moveName = message.moveAt(2)?.displayName ?: run { "(Unrecognized: ${message.argumentAt(2)})".text() }
val lang = when (effectID) {
// TODO: in the games they use a generic image because there is a popup of the ability and the sprite of the mon, it may be good to have a similar system here
"armortail", "damp", "dazzling", "queenlymajesty" -> battleLang("cant.generic", name, moveName)
"par", "slp", "frz" -> {
val status = Statuses.getStatus(effectID)?.name?.path ?: return@dispatchWaiting
lang("status.$status.is", name)
}
else -> battleLang("cant.$effectID", name, moveName)
}
battle.broadcastChatMessage(lang.red())
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,26 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-clearallboost|
*
* Clears all boosts from all Pokémon on both sides.
* @author Segfault Guy
* @since April 26, 2023
*/
class ClearAllBoostInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
battle.activePokemon.forEach {
it.battlePokemon?.contextManager?.clear(BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
}
battle.broadcastChatMessage(battleLang("clearallboost"))
}
}
}
@@ -0,0 +1,34 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-clearnegativeboost|POKEMON
*
* Clear the negative boosts from the target POKEMON (usually as the result of a zeffect).
* @author Segfault Guy
* @since September 10, 2023
*/
class ClearNegativeBoostInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val battlePokemon = message.battlePokemon(0, battle) ?: return
val pokemonName = battlePokemon.getName()
battle.dispatchWaiting(1.5F) {
val lang = when {
message.hasOptionalArgument("zeffect") -> battleLang("clearallnegativeboost.zeffect", pokemonName)
else -> battleLang("clearallnegativeboost", pokemonName)
}
if (!message.hasOptionalArgument("silent")) {
battle.broadcastChatMessage(lang)
}
battlePokemon.contextManager.clear(BattleContext.Type.UNBOOST)
battle.minorBattleActions[battlePokemon.uuid] = message
}
}
}
@@ -0,0 +1,31 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-copyboost|SOURCE|TARGET|(from)EFFECT
*
* Copies any stat changes from the TARGET Pokémon to the SOURCE Pokémon due to EFFECT.
* @author JMMCP
* @since November 27, 2023
*/
class CopyBoostInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val targetPokemon = message.battlePokemon(1, battle) ?: return@dispatchWaiting
val targetPokemonName = targetPokemon.getName()
val lang = battleLang("copyboost.generic", pokemonName, targetPokemonName)
battle.broadcastChatMessage(lang)
pokemon.contextManager.copy(targetPokemon.contextManager, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,34 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-crit|POKEMON
*
* POKEMON received a critical hit.
* @author Hunter
* @since August 18, 2022
*/
class CritInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchGo
battle.broadcastChatMessage(battleLang("crit").yellow())
ShowdownInterpreter.lastCauser[battle.battleId]?.let { message ->
val battlePokemon = message.battlePokemon(0, battle) ?: return@let
if (LastBattleCriticalHitsEvolutionProgress.supports(battlePokemon.effectedPokemon)) {
val progress = battlePokemon.effectedPokemon.evolutionProxy.current().progressFirstOrCreate({ it is LastBattleCriticalHitsEvolutionProgress }) { LastBattleCriticalHitsEvolutionProgress() }
progress.updateProgress(LastBattleCriticalHitsEvolutionProgress.Progress(progress.currentProgress().amount + 1))
}
}
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,50 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.interpreter.Effect
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattlePersistentStatusPacket
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-curestatus|POKEMON|STATUS
*
* The POKEMON has recovered from its STATUS.
* @author Hiroku
* @since November 5, 2022
*/
class CureStatusInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val maybeActivePokemon = message.actorAndActivePokemon(0, battle)?.second?.battlePokemon
val maybePartyPokemon = message.battlePokemon(0, battle)
val pokemon = maybeActivePokemon ?: maybePartyPokemon ?: return
val pokemonName = pokemon.getName()
val status = message.argumentAt(1)?.let(Statuses::getStatus) ?: return
val effect = message.effect()
ShowdownInterpreter.broadcastOptionalAbility(battle, effect, pokemonName)
battle.dispatchWaiting {
pokemon.effectedPokemon.status = null
pokemon.sendUpdate()
if (maybeActivePokemon != null) {
message.pnxAndUuid(0)?.let {
battle.sendUpdate(BattlePersistentStatusPacket(it.first, null))
}
}
val lang = when (effect?.type) {
Effect.Type.ABILITY -> battleLang("curestatus.${effect.id}", pokemonName)
else -> status.removeMessage.asTranslated(pokemonName)
}
battle.broadcastChatMessage(lang)
pokemon.contextManager.remove(status.showdownName, BattleContext.Type.STATUS)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.battles.dispatch.WaitDispatch
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.animation.PlayPoseableAnimationPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
@@ -30,6 +31,13 @@ import java.util.concurrent.CompletableFuture
import kotlin.math.roundToInt
import net.minecraft.text.Text
/**
* Format: |-damage|POKEMON|HP STATUS
*
* POKEMON has taken damage and is now at HP STATUS
* @author Hiroku
* @since March 11, 2022
*/
class DamageInstruction(
val instructionSet: InstructionSet,
val actor: BattleActor,
@@ -37,10 +45,10 @@ class DamageInstruction(
val privateMessage: BattleMessage
) : InterpreterInstruction {
var future = CompletableFuture.completedFuture(Unit)
val battlePokemon = publicMessage.getBattlePokemon(0, actor.battle)
val expectedTarget = publicMessage.battlePokemon(0, actor.battle)
override fun invoke(battle: PokemonBattle) {
battlePokemon ?: return
val battlePokemon = publicMessage.battlePokemon(0, actor.battle) ?: return
val recoiling = privateMessage.optionalArgument("from")?.equals("recoil", true) == true
val lastCauser = instructionSet.getMostRecentCauser(comparedTo = this)
if (recoiling) {
@@ -59,7 +67,7 @@ class DamageInstruction(
val newHealth = privateMessage.argumentAt(1)?.split(" ")?.get(0) ?: return
val effect = privateMessage.effect()
val pokemonName = battlePokemon.getName()
val sourceName = privateMessage.getSourceBattlePokemon(battle)?.getName() ?: Text.literal("UNKNOWN")
val sourceName = privateMessage.battlePokemonFromOptional(battle)?.getName() ?: Text.literal("UNKNOWN")
var causedFaint = newHealth == "0"
battle.dispatch {
@@ -1,19 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
class DeprecatedInstruction(val message: BattleMessage, val function: (PokemonBattle, BattleMessage, MutableList<String>) -> Unit) : InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
function(battle, message, mutableListOf())
}
}
@@ -1,25 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
class DeprecatedSplitInstruction(
val battleActor: BattleActor,
val publicMessage: BattleMessage,
val privateMessage: BattleMessage,
val function: (PokemonBattle, BattleActor, BattleMessage, BattleMessage) -> Unit
) : InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
function(battle, battleActor, publicMessage, privateMessage)
}
}
@@ -0,0 +1,26 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |detailschange|POKEMON|DETAILS|HP STATUS
*
* POKEMON has changed formes permanently (i.e. Mega Evolution) to DETAILS.
* @author Segfault Guy
* @since September 10, 2023
*/
class DetailsChangeInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val battlePokemon = message.battlePokemon(0, battle) ?: return
val pokemonName = battlePokemon.getName()
val formName = message.argumentAt(1)?.split(',')?.get(0)?.substringAfter('-')?.lowercase() ?: return
battle.dispatchWaiting {
battle.broadcastChatMessage(battleLang("detailschange.$formName", pokemonName))
battle.majorBattleActions[battlePokemon.uuid] = message
}
}
}
@@ -0,0 +1,50 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.battles.dispatch.BattleDispatch
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |drag|POKEMON|DETAILS|HP STATUS
*
* POKEMON has switched in (if there was an old Pokémon in that position, it is switched out).
* POKEMON|DETAILS represents all the information that can be used to tell Pokémon apart.
* The switched Pokémon has HP health points and STATUS status.
* @author
*/
class DragInstruction(val instructionSet: InstructionSet, val battleActor: BattleActor, val publicMessage: BattleMessage, val privateMessage: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchInsert {
val (pnx, _) = publicMessage.pnxAndUuid(0)!!
val (_, activePokemon) = battle.getActorAndActiveSlotFromPNX(pnx)
val illusion = publicMessage.battlePokemonFromOptional(battle , "is")
val pokemon = publicMessage.battlePokemon(0, battle) ?: return@dispatchInsert emptySet()
battle.broadcastChatMessage(battleLang("dragged_out", pokemon.getName()))
activePokemon.battlePokemon?.let { oldPokemon ->
oldPokemon.contextManager.clear(BattleContext.Type.VOLATILE, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
battle.majorBattleActions[oldPokemon.uuid] = publicMessage
}
battle.majorBattleActions[pokemon.uuid] = publicMessage
val entity = if (battleActor is EntityBackedBattleActor<*>) battleActor.entity else null
setOf(
BattleDispatch {
if (entity != null) {
SwitchInstruction.createEntitySwitch(battle, battleActor, entity, pnx, activePokemon, pokemon, illusion)
} else {
SwitchInstruction.createNonEntitySwitch(battle, battleActor, pnx, activePokemon, pokemon, illusion)
}
}
)
}
}
}
@@ -0,0 +1,27 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-endability|POKEMON
*
* The POKEMON's Ability was suppressed.
* @author jeffw773
* @since November 28, 2023
*/
class EndAbilityInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val lang = battleLang("endability", pokemonName)
battle.broadcastChatMessage(lang)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,36 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.lang
/**
* Format: |-end|POKEMON|EFFECT
*
* The volatile status from EFFECT inflicted on POKEMON has ended.
* @author Hiroku
* @since October 3, 2022
*/
class EndInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val effectID = message.effectAt(1)?.id ?: return@dispatchWaiting
if (!message.hasOptionalArgument("silent")) {
val lang = when (effectID) {
"yawn" -> lang("status.sleep.apply", pokemonName)
else -> battleLang("end.$effectID", pokemonName)
}
battle.broadcastChatMessage(lang)
}
pokemon.contextManager.remove(effectID, BattleContext.Type.VOLATILE)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,37 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-enditem|POKEMON|ITEM|(from)EFFECT
*
* ITEM held by POKEMON has been destroyed by a move or ability EFFECT, and it now holds no item.
*
* Alt format: |-enditem|POKEMON|ITEM
*
* POKEMON's ITEM has destroyed itself (consumed or used).
* @author Licious
* @since December 30, 2022
*/
class EndItemInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
// All logic regarding broadcasting battle messages is handled in CobblemonHeldItemManager
val battlePokemon = message.battlePokemon(0, battle) ?: return@dispatchGo
val itemEffect = message.effectAt(1) ?: return@dispatchGo
battlePokemon.heldItemManager.handleEndInstruction(battlePokemon, battle, message)
battle.minorBattleActions[battlePokemon.uuid] = message
battlePokemon.contextManager.remove(itemEffect.id, BattleContext.Type.ITEM)
if (message.hasOptionalArgument("eat")) {
battlePokemon.entity?.playSound(CobblemonSounds.BERRY_EAT, 1F, 1F)
}
}
}
}
@@ -0,0 +1,35 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattleMadeInvalidChoicePacket
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |error|(Invalid choice) ERROR
*
* The actor needs to send a different decision due to ERROR.
* @author Yaseen
* @since April 22, 2023
*/
class ErrorInstruction(val battleActor: BattleActor, val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.log("Error Instruction")
battle.dispatchGo {
//TODO: some lang stuff for the error messages (Whats the protocol for adding to other langs )
//Also is it okay to ignore the team preview error for now? - You bet!
val lang = when(message.rawMessage) {
"|error|[Unavailable choice] Can't switch: The active Pokémon is trapped" -> battleLang("error.pokemon_is_trapped").red()
"|error|[Invalid choice] Can't choose for Team Preview: You're not in a Team Preview phase" -> return@dispatchGo
else -> battle.createUnimplemented(message)
}
battleActor.sendMessage(lang)
battleActor.mustChoose = true
battleActor.sendUpdate(BattleMadeInvalidChoicePacket())
}
}
}
@@ -0,0 +1,48 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-fail|POKEMON|ACTION
*
* ACTION has failed against POKEMON.
* @author Hunter
* @since September 25, 2022
*/
class FailInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F){
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val effectID = message.effectAt(1)?.id
val cause = message.effect("from")
val of = message.battlePokemonFromOptional(battle)
val lang = when (effectID) {
null, "burnup", "doubleshock" -> battleLang("fail") // Moves that use default fail lang. (Null included for moves that fail with no effect, for example: Baton Pass.)
"shedtail" -> battleLang("fail.substitute", pokemonName)
"hyperspacefury", "aurawheel" -> battleLang("fail.darkvoid", pokemonName) // Moves that can only be used by one species and fail when any others try
"corrosivegas" -> battleLang("fail.healblock", pokemonName)
"dynamax" -> battleLang("fail.grassknot", pokemonName) // Covers weight moves that fail against dynamaxed Pokémon
"unboost" -> {
val statKey = message.argumentAt(2)
val stat = statKey?.let { Stats.getStat(it).displayName }
if (stat != null) {
battleLang("fail.$effectID.single", pokemonName, stat)
} else {
battleLang("fail.$effectID", pokemonName)
}
}
else -> battleLang("fail.$effectID", pokemonName)
}
battle.broadcastChatMessage(lang.red())
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -19,9 +19,16 @@ import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattleFaintPacket
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |faint|POKEMON
*
* POKEMON has fainted.
* @author Deltric
* @since January 22, 2022
*/
class FaintInstruction(battle: PokemonBattle, val message: BattleMessage) : InterpreterInstruction {
var waitTime = 2.5F
val faintingPokemon = message.getBattlePokemon(0, battle)!!
val faintingPokemon = message.battlePokemon(0, battle)!!
override fun invoke(battle: PokemonBattle) {
val faintMessage = battleLang("fainted", faintingPokemon.getName()).red()
@@ -0,0 +1,33 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-fieldactivate|EFFECT
*
* A miscellaneous EFFECT has activated for the entire field.
* @author Segfault Guy
* @since April 5, 2023
*/
class FieldActivateInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(2.5F) {
val effectID = message.effectAt(0)?.id ?: return@dispatchWaiting
val lang = battleLang("fieldactivate.$effectID")
battle.broadcastChatMessage(lang.red())
// share this action with all active Pokemon
battle.activePokemon.forEach {
it.battlePokemon?.contextManager?.addUnique(ShowdownInterpreter.getContextFromAction(message, BattleContext.Type.VOLATILE, battle))
}
}
}
}
@@ -0,0 +1,31 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-fieldend|CONDITION
*
* The field CONDITION has ended.
* @author Licious
* @since February 8, 2023
*/
class FieldEndInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
val effect = message.effectAt(0) ?: return@dispatchWaiting
val lang = battleLang("fieldend.${effect.id}")
battle.broadcastChatMessage(lang)
val type = BattleContext.Type.valueOf(effect.rawData.substringAfterLast(" ").uppercase())
battle.contextManager.remove(effect.id, type)
}
}
}
@@ -0,0 +1,34 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
import net.minecraft.text.Text
/**
* Format: |-fieldstart|CONDITION
*
* The field CONDITION has started.
* @author Xylopia
* @since January 31, 2023
*/
class FieldStartInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val effect = message.effectAt(0) ?: return
val user = message.battlePokemonFromOptional(battle)?.getName() ?: Text.literal("UNKNOWN")
ShowdownInterpreter.broadcastOptionalAbility(battle, effect, user)
battle.dispatchWaiting(1.5F) {
// Note persistent is a CAP ability only we can ignore the flag
val lang = battleLang("fieldstart.${effect.id}", user)
battle.broadcastChatMessage(lang)
val type = BattleContext.Type.valueOf(effect.rawData.substringAfterLast(" ").uppercase())
battle.contextManager.add(ShowdownInterpreter.getContextFromAction(message, type, battle))
}
}
}
@@ -0,0 +1,91 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.interpreter.Effect
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.data.ShowdownIdentifiable
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattlePersistentStatusPacket
import com.cobblemon.mod.common.pokemon.status.PersistentStatus
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-heal|POKEMON|HP STATUS
*
* POKEMON has healed damage and is now at HP STATUS.
* @author Licious
* @since February 6, 2023
*/
class HealInstruction(val actor: BattleActor, val publicMessage: BattleMessage, val privateMessage: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val pnx = privateMessage.pnxAndUuid(0)?.first
val battlePokemon = privateMessage.battlePokemon(0, battle) ?: return
val rawHpAndStatus = privateMessage.argumentAt(1)?.split(" ") ?: return
val rawHpRatio = rawHpAndStatus.getOrNull(0) ?: return
val newHealth = rawHpRatio.split("/").map { it.toFloatOrNull() ?: return }
val newHealthRatio = rawHpRatio.split("/").map { it.toFloatOrNull()?.div(newHealth[1]) ?: return }
val effect = privateMessage.effect()
val pokemonName = battlePokemon.getName()
ShowdownInterpreter.broadcastOptionalAbility(battle, effect, pokemonName)
battle.dispatchWaiting {
if (pnx != null) {
// dynamax changes max health
battle.sendSidedUpdate(actor, BattleHealthChangePacket(pnx, newHealth[0], newHealth[1]), BattleHealthChangePacket(pnx, newHealthRatio[0]))
}
val silent = privateMessage.hasOptionalArgument("silent")
if (!silent) {
val lang = when {
privateMessage.hasOptionalArgument("zeffect") -> battleLang("heal.zeffect", battlePokemon.getName())
privateMessage.hasOptionalArgument("wisher") -> {
val name = privateMessage.optionalArgument("wisher")!!
val showdownId = name.lowercase().replace(ShowdownIdentifiable.REGEX, "")
val wisher = actor.pokemonList.firstOrNull { it.effectedPokemon.showdownId() == showdownId }
// If no Pokémon is found this is a nickname
battleLang("heal.wish", wisher?.getName() ?: actor.nameOwned(name))
}
privateMessage.hasOptionalArgument("from") -> {
when (effect!!.type) {
Effect.Type.ITEM -> when (effect.id) {
"leftovers", "shellbell", "blacksludge" -> battleLang("heal.leftovers", battlePokemon.getName(), effect.typelessData)
else -> battleLang("heal.item", battlePokemon.getName(), effect.typelessData)
}
else -> when (effect.id) {
"drain" -> {
val drained = privateMessage.battlePokemonFromOptional(battle) ?: return@dispatchWaiting
battleLang("heal.drain", drained.getName())
}
else -> battleLang("heal.${effect.id}", battlePokemon.getName())
}
}
}
else -> {
battleLang("heal.generic", battlePokemon.getName())
}
}
battle.broadcastChatMessage(lang)
}
battle.minorBattleActions[battlePokemon.uuid] = privateMessage
battlePokemon.effectedPokemon.currentHealth = newHealth[0].toInt()
// This part is not always present
val rawStatus = rawHpAndStatus.getOrNull(1) ?: return@dispatchWaiting
val status = Statuses.getStatus(rawStatus) ?: return@dispatchWaiting
if (status is PersistentStatus && battlePokemon.effectedPokemon.status?.status != status) {
battlePokemon.effectedPokemon.applyStatus(status)
if (pnx != null) {
battle.sendUpdate(BattlePersistentStatusPacket(pnx, status))
}
if (!silent) {
status.applyMessage.let { battle.broadcastChatMessage(it.asTranslated(battlePokemon.getName())) }
}
}
}
}
}
@@ -0,0 +1,26 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-hitcount|POKEMON|NUM
*
* A multi-hit move hit the POKEMON NUM times.
* @author Licious
* @since December 30, 2022
*/
class HitCountInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
val battlePokemon = message.battlePokemon(0, battle) ?: return@dispatchGo
val hitCount = message.argumentAt(1)?.toIntOrNull() ?: return@dispatchGo
val lang = if (hitCount == 1) battleLang("hit_count_singular") else battleLang("hit_count", hitCount)
battle.minorBattleActions[battlePokemon.uuid] = message
battle.broadcastChatMessage(lang)
}
}
}
@@ -11,6 +11,12 @@ package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
/**
* Catch-all for instruction from Showdown that are not parsed.
*
* @author Hiroku
* @since December 25, 2023
*/
class IgnoredInstruction : InterpreterInstruction {
override fun invoke(pokemonBattle: PokemonBattle) = Unit
}
@@ -0,0 +1,26 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-immune|POKEMON
*
* POKEMON was immune to a move.
* @author Hiroku
* @since October 3, 2022
*/
class ImmuneInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val name = pokemon.getName()
battle.broadcastChatMessage(battleLang("immune", name).red())
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,41 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BasicContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-invertboost|POKEMON
*
* POKEMON had its stat changes inverted.
* @author JMMCP
* @since November 26, 2023
*/
class InvertBoostInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val name = pokemon.getName()
battle.broadcastChatMessage(battleLang("invertboost", name))
// update and invert BOOST and UNBOOST contexts
val context = ShowdownInterpreter.getContextFromAction(message, BattleContext.Type.BOOST, battle)
val newUnboosts = pokemon.contextManager.get(BattleContext.Type.BOOST)?.map {
BasicContext(it.id, context.turn, BattleContext.Type.UNBOOST, context.origin)
}?.toTypedArray()
val newBoosts = pokemon.contextManager.get(BattleContext.Type.UNBOOST)?.map {
BasicContext(it.id, context.turn, BattleContext.Type.BOOST, context.origin)
}?.toTypedArray()
pokemon.contextManager.clear(BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
newBoosts?.let { pokemon.contextManager.add(*it) }
newUnboosts?.let { pokemon.contextManager.add(*it) }
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,34 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import net.minecraft.text.Text
/**
* Format: |-item|POKEMON|ITEM|(from)EFFECT
*
* ITEM held by POKEMON has been changed or revealed due to a move or ability EFFECT.
*
* Alt format: |-item|POKEMON|ITEM
*
* POKEMON has just switched in, and its ITEM is being announced to have a long-term effect.
* @author Licious
* @since December 30, 2022
*/
class ItemInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val sourceName = message.battlePokemonFromOptional(battle)?.getName() ?: Text.literal("UNKOWN")
ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), sourceName)
battle.dispatchGo {
val battlePokemon = message.battlePokemon(0, battle) ?: return@dispatchGo
battlePokemon.heldItemManager.handleStartInstruction(battlePokemon, battle, message)
battle.minorBattleActions[battlePokemon.uuid] = message
battlePokemon.contextManager.add(ShowdownInterpreter.getContextFromAction(message, BattleContext.Type.ITEM, battle))
}
}
}
@@ -0,0 +1,26 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-mega|POKEMON|MEGASTONE
*
* POKEMON used item MEGASTONE to Mega Evolve.
* @author Segfault Guy
* @since September 10, 2023
*/
class MegaInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val battlePokemon = message.battlePokemon(0, battle) ?: return
val pokemonName = battlePokemon.getName()
battle.dispatchWaiting {
battle.broadcastChatMessage(battleLang("mega", pokemonName).yellow())
battle.minorBattleActions[battlePokemon.uuid] = message
}
}
}
@@ -11,22 +11,22 @@ package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format:
* |-miss|SOURCE|TARGET
* Format: |-miss|SOURCE|TARGET
*
* The move used by the SOURCE Pokémon missed (maybe absent) the TARGET Pokémon.
* @author Hiroku
* @since October 3, 2022
*/
class MissInstruction(val battle: PokemonBattle, val instructionSet: InstructionSet, val message: BattleMessage): InterpreterInstruction {
val target = message.getBattlePokemon(1, battle)
class MissInstruction(val battle: PokemonBattle, val message: BattleMessage): InterpreterInstruction {
val target = message.battlePokemon(1, battle)
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
val pokemon = message.getBattlePokemon(0, battle) ?: return@dispatchGo
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchGo
battle.broadcastChatMessage(battleLang("missed").red())
battle.minorBattleActions[pokemon.uuid] = message
}
@@ -31,6 +31,13 @@ import com.cobblemon.mod.common.pokemon.evolution.progress.UseMoveEvolutionProgr
import com.cobblemon.mod.common.util.battleLang
import java.util.concurrent.CompletableFuture
/**
* Format: |move|POKEMON|MOVE|TARGET
*
* POKEMON has used MOVE at TARGET.
* @author Deltric
* @since January 22, 2022
*/
class MoveInstruction(
val instructionSet: InstructionSet,
val message: BattleMessage
@@ -46,8 +53,8 @@ class MoveInstruction(
var targetPokemon: BattlePokemon? = null
override fun invoke(battle: PokemonBattle) {
userPokemon = message.getBattlePokemon(0, battle)!!
targetPokemon = message.getBattlePokemon(2, battle)
userPokemon = message.battlePokemon(0, battle)!!
targetPokemon = message.battlePokemon(2, battle)
val targetPokemon = targetPokemon // So smart non-null casts can happen
val optionalEffect = message.effect()
@@ -103,7 +110,7 @@ class MoveInstruction(
}
}
val hurtTargets = subsequentInstructions.filterIsInstance<DamageInstruction>().mapNotNull { it.battlePokemon }
val hurtTargets = subsequentInstructions.filterIsInstance<DamageInstruction>().mapNotNull { it.expectedTarget }
runtime.environment.getQueryStruct().addFunction("hurt") { params ->
if (params.params.size == 0) {
return@addFunction DoubleValue(hurtTargets.isNotEmpty())
@@ -0,0 +1,19 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-nothing
*
* Pathetic. Your move did absolutely nothing. You should feel bad for being bad.
* @author Hiroku
* @since August 20, 2022
*/
class NothingInstruction() : InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo { battle.broadcastChatMessage(battleLang("nothing")) }
}
}
@@ -0,0 +1,30 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
/**
* Format: |pp_update|<side_id>: <pokemon_uuid>|...<move_id>: <move_pp>
*
* @author Licious
* @since September 22, 2022
*/
class PpUpdateInstruction( val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatch {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatch GO
val moveDatum = message.argumentAt(1)?.split(", ") ?: return@dispatch GO
moveDatum.forEach { moveData ->
val moveIdAndPp = moveData.split(": ")
val moveId = moveIdAndPp[0]
val movePp = moveIdAndPp[1]
val move = pokemon.effectedPokemon.moveSet.firstOrNull { move -> move.name.equals(moveId, true) } ?: return@dispatch GO
move.currentPp = movePp.toInt()
}
GO
}
}
}
@@ -0,0 +1,32 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-prepare|ATTACKER|MOVE and |-prepare|ATTACKER|MOVE|DEFENDER
*
* ATTACKER Pokémon is preparing to use a charge MOVE on DEFENDER or an unknown target.
* @author Renaissance
* @since March 24, 2023
*/
class PrepareInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val effectID = message.effectAt(1)?.id ?: return@dispatchWaiting
//Prevents spam when the move Role Play is used
val lang = when (effectID) {
"shadowforce" -> battleLang("prepare.phantomforce", pokemonName) //Phantom Force and Shadow Force share the same text
"solarblade" -> battleLang("prepare.solarbeam", pokemonName) //Solar Beam and Solar Blade share the same text
else -> battleLang("prepare.$effectID", pokemonName)
}
battle.broadcastChatMessage(lang)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,24 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-mustrecharge|POKEMON
*
* POKEMON must spend the turn recharging from a previous move.
* @author Hunter
* @since September 25, 2022
*/
class RechargeInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(2F){
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
battle.broadcastChatMessage(battleLang("recharge", pokemon.getName()))
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,30 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattleSwitchPokemonPacket
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |replace|POKEMON|DETAILS|HP STATUS
*
* Illusion has ended for POKEMON. Syntax is the same as [SwitchInstruction].
* @author Segfault Guy
* @since March 18th, 2024
*/
class ReplaceInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val entity = pokemon.entity
val actor = pokemon.actor
val pnx = message.pnxAndUuid(0)?.first
entity?.let { it.effects.mockEffect?.end(it) }
pnx?.let {battle.sendSidedUpdate(actor, BattleSwitchPokemonPacket(pnx, pokemon, true, null), BattleSwitchPokemonPacket(pnx, pokemon, false, null)) }
}
}
}
@@ -0,0 +1,49 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.ShowdownActionRequest
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
/**
* Format: |request|REQUEST
*
* It's time for the actor to make a decision.
* @author Deltric
* @since January 22, 2022
*/
class RequestInstruction(val battleActor: BattleActor, val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.log("Request Instruction")
if (message.rawMessage.contains("teamPreview")) // TODO probably change when we're allowing team preview
return
// Parse Json message and update state info for actor
val request = BattleRegistry.gson.fromJson(message.rawMessage.split("|request|")[1], ShowdownActionRequest::class.java)
request.sanitize(battle, battleActor)
if (battle.started) {
battle.dispatchGo {
// This request won't be acted on until the start of next turn
battleActor.sendUpdate(BattleQueueRequestPacket(request))
battleActor.request = request
battleActor.responses.clear()
// We need to send this out because 'upkeep' isn't received until the request is handled since the turn won't swap
if (request.forceSwitch.contains(true)) {
battle.doWhenClear {
battleActor.mustChoose = true
battleActor.sendUpdate(BattleMakeChoicePacket())
}
}
}
} else {
battleActor.request = request
battleActor.responses.clear()
}
}
}
@@ -0,0 +1,24 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-resisted|POKEMON
*
* POKEMON resisted the attack.
* @author
* @since
*/
class ResistedInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchGo
battle.broadcastChatMessage(battleLang("resisted"))
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,30 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-setboost|POKEMON|STAT|AMOUNT
*
* Same as -boost and -unboost, but STAT is set to AMOUNT instead of boosted by AMOUNT.\
* @author Renaissance
* @since February 16, 2023
*/
class SetBoostInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val effectID = message.effect()?.id ?: return@dispatchWaiting
val lang = battleLang("setboost.$effectID", pokemonName)
battle.broadcastChatMessage(lang)
pokemon.contextManager.add(ShowdownInterpreter.getContextFromAction(message, BattleContext.Type.BOOST, battle))
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,37 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import com.cobblemon.mod.common.util.battleLang
import kotlin.math.roundToInt
/**
* Format: |-sethp|POKEMON|HP
*
* POKEMON now has HP hit points.
* @author Licious
* @since February 8, 2023
*/
class SetHpInstruction(val actor: BattleActor, val publicMessage: BattleMessage, val privateMessage: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting {
val (pnx, _) = privateMessage.pnxAndUuid(0) ?: return@dispatchWaiting
val flatHp = privateMessage.argumentAt(1)?.split("/")?.getOrNull(0)?.toFloatOrNull() ?: return@dispatchWaiting
val ratioHp = publicMessage.argumentAt(1)?.split("/")?.getOrNull(0)?.toFloatOrNull()?.times(0.01F) ?: return@dispatchWaiting
val battlePokemon = privateMessage.battlePokemon(0, battle) ?: return@dispatchWaiting
battlePokemon.effectedPokemon.currentHealth = flatHp.roundToInt()
battle.sendSidedUpdate(actor, BattleHealthChangePacket(pnx, flatHp), BattleHealthChangePacket(pnx, ratioHp))
// It doesn't matter which we check when silent both have it
if (!publicMessage.hasOptionalArgument("silent")) {
val effectID = publicMessage.effect()?.id ?: return@dispatchWaiting
val lang = battleLang("sethp.$effectID")
battle.broadcastChatMessage(lang)
}
battle.minorBattleActions[battlePokemon.uuid] = publicMessage
}
}
}
@@ -0,0 +1,37 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-sideend|SIDE|CONDITION
*
* Indicates that the side CONDITION ended for the given SIDE.
* @author Segfault Guy
* @since April 5, 2023
*/
class SideEndInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(2F) {
val side = if (message.argumentAt(0)?.get(1) == '1') battle.side1 else battle.side2
val effect = message.effectAt(1) ?: return@dispatchWaiting
battle.sides.forEach {
val subject = if (it == side) battleLang("side_subject.ally") else battleLang("side_subject.opponent")
val lang = battleLang("sideend.${effect.id}", subject)
it.broadcastChatMessage(lang)
}
val bucket = when(effect.rawData.substringAfterLast(" ").lowercase()) {
"reflect", "screen", "veil" -> BattleContext.Type.SCREEN
"spikes", "rock", "web" -> BattleContext.Type.HAZARD
"tailwind" -> BattleContext.Type.TAILWIND
else -> BattleContext.Type.MISC
}
side.contextManager.remove(effect.id, bucket)
}
}
}
@@ -0,0 +1,38 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-sidestart|SIDE|CONDITION
*
* A side CONDITION has started on SIDE.
* @author Segfault Guy
* @since April 5, 2023
*/
class SideStartInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(2F) {
val side = if (message.argumentAt(0)?.get(1) == '1') battle.side1 else battle.side2
val effect = message.effectAt(1) ?: return@dispatchWaiting
battle.sides.forEach {
val subject = if (it == side) battleLang("side_subject.ally") else battleLang("side_subject.opponent")
val lang = battleLang("sidestart.${effect.id}", subject)
it.broadcastChatMessage(lang)
}
val bucket = when(effect.rawData.substringAfterLast(" ").lowercase()) {
"reflect", "screen", "veil" -> BattleContext.Type.SCREEN
"spikes", "rock", "web" -> BattleContext.Type.HAZARD
"tailwind" -> BattleContext.Type.TAILWIND
else -> BattleContext.Type.MISC
}
side.contextManager.add(ShowdownInterpreter.getContextFromAction(message, bucket, battle))
}
}
}
@@ -0,0 +1,17 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/** Placeholder for instructions that only */
class SilentInstruction(val instructionSet: InstructionSet, val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo { }
}
}
@@ -0,0 +1,27 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-singlemove|POKEMON|MOVE
*
* POKEMON used MOVE which causes a temporary effect lasting the duration of the move.
* @author Renaissance
* @since March 24, 2023
*/
class SingleMoveInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val effectID = message.effectAt(1)?.id ?: return@dispatchWaiting
val lang = battleLang("singlemove.$effectID", pokemonName)
battle.broadcastChatMessage(lang)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,29 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
import net.minecraft.text.Text
/**
* Format: |-singleturn|POKEMON|MOVE
*
* POKEMON used MOVE which causes a temporary effect lasting the duration of the turn.
* @author Renaissance
* @since March 24, 2023
*/
class SingleTurnInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val sourceName = message.battlePokemonFromOptional(battle)?.getName() ?: Text.literal("UNKOWN")
val effectID = message.effectAt(1)?.id ?: return@dispatchWaiting
val lang = battleLang("singleturn.$effectID", pokemonName, sourceName)
battle.broadcastChatMessage(lang)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,57 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.dispatch.WaitDispatch
import com.cobblemon.mod.common.util.battleLang
import net.minecraft.text.Text
/**
* Format: |-start|POKEMON|EFFECT
*
* A volatile status has been inflicted on POKEMON by EFFECT.
* @author Deltric
* @since January 21, 2022
*/
class StartInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatch {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatch GO
val effectID = message.effectAt(1)?.id ?: return@dispatch GO
val optionalEffect = message.effect()
val optionalPokemon = message.battlePokemonFromOptional(battle)
val optionalPokemonName = optionalPokemon?.getName()
val extraEffect = message.effectAt(2)?.typelessData ?: Text.literal("UNKOWN")
// skip adding contexts for every time the perish counter decrements
if (!effectID.contains("perish")) {
// don't need to add unique: showdown won't send -start instruction if volatile status is already present
pokemon.contextManager.add(ShowdownInterpreter.getContextFromAction(message, BattleContext.Type.VOLATILE, battle))
}
battle.minorBattleActions[pokemon.uuid] = message
if (!message.hasOptionalArgument("silent")) {
val lang = if (optionalEffect?.id == "reflecttype" && optionalPokemonName != null)
battleLang("start.reflecttype", pokemon.getName(), optionalPokemonName)
else
when (effectID) {
"confusion", "perish3" -> return@dispatch GO // Skip
"perish2", "perish1", "perish0",
"stockpile1", "stockpile2", "stockpile3" -> battleLang("start.${effectID.dropLast(1)}", pokemon.getName(), effectID.last().digitToInt())
"dynamax" -> battleLang("start.${message.effectAt(2)?.id ?: effectID}", pokemon.getName()).yellow()
"curse" -> battleLang("start.curse", message.battlePokemonFromOptional(battle)!!.getName(), pokemon.getName())
else -> battleLang("start.$effectID", pokemon.getName(), extraEffect)
}
battle.broadcastChatMessage(lang)
}
WaitDispatch(1F)
}
}
}
@@ -0,0 +1,45 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattlePersistentStatusPacket
import com.cobblemon.mod.common.pokemon.status.PersistentStatus
import com.cobblemon.mod.common.util.asTranslated
/**
* Format: |-status|POKEMON|STATUS
*
* POKEMON has been inflicted with STATUS.
* @author Hiroku
* @since October 3, 2022
*/
class StatusInstruction( val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val (pnx, _) = message.pnxAndUuid(0) ?: return
val pokemon = message.battlePokemon(0, battle) ?: return
val otherPokemon = message.actorAndActivePokemonFromOptional(battle, "of")?.second?.battlePokemon
val statusLabel = message.argumentAt(1) ?: return
val status = Statuses.getStatus(statusLabel) ?: return Cobblemon.LOGGER.error("Unrecognized status: $statusLabel")
if (otherPokemon == null)
ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), pokemon.getName())
else
ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), otherPokemon.getName())
battle.dispatchWaiting {
if (status is PersistentStatus) {
pokemon.effectedPokemon.applyStatus(status)
battle.sendUpdate(BattlePersistentStatusPacket(pnx, status))
}
battle.broadcastChatMessage(status.applyMessage.asTranslated(pokemon.getName()))
pokemon.contextManager.add(ShowdownInterpreter.getContextFromAction(message, BattleContext.Type.STATUS, battle))
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,24 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-supereffective|POKEMON
*
* A move was super effective against POKEMON.
* @author Hunter
* @since August 18, 2022
*/
class SuperEffectiveInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchGo
battle.broadcastChatMessage(battleLang("superEffective"))
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,36 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-swapboost|SOURCE|TARGET|STATS
*
* Swaps the boosts from STATS between the SOURCE Pokémon and TARGET Pokémon.
* @author Renaissance
* @since March 24, 2023
*/
class SwapBoostInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(2F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val targetPokemon = message.battlePokemon(1, battle) ?: return@dispatchWaiting
val targetPokemonName = targetPokemon.getName()
val effectID = message.effect()?.id ?: return@dispatchWaiting
val lang = when (effectID) {
"guardswap", "powerswap", "heartswap" -> battleLang("swapboost.$effectID", pokemonName)
else -> battleLang("swapboost.generic", pokemonName, targetPokemonName)
}
battle.broadcastChatMessage(lang)
pokemon.contextManager.swap(targetPokemon.contextManager, BattleContext.Type.BOOST)
pokemon.contextManager.swap(targetPokemon.contextManager, BattleContext.Type.UNBOOST)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -0,0 +1,30 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
/**
* Format: |-swapsideconditions
*
* Swaps side conditions between sides. Used for Court Change.
* @author Segfault Guy
* @since February 19, 2024
*/
class SwapSideConditionsInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
val sides = mutableListOf<BattleSide>()
battle.sides.forEach { side ->
if (!sides.contains(side)) {
val oppositeManager = side.getOppositeSide().contextManager
side.contextManager.swap(oppositeManager, BattleContext.Type.TAILWIND, BattleContext.Type.SCREEN, BattleContext.Type.HAZARD)
}
sides.add(side)
}
}
}
}
@@ -0,0 +1,135 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.dispatch.*
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.pokemon.effects.IllusionEffect
import com.cobblemon.mod.common.net.messages.client.battle.BattleSwitchPokemonPacket
import com.cobblemon.mod.common.util.swap
import net.minecraft.entity.LivingEntity
import net.minecraft.server.world.ServerWorld
import java.util.concurrent.CompletableFuture
/**
* Format: |switch|POKEMON|DETAILS|HP STATUS or |drag|POKEMON|DETAILS|HP STATUS
*
* POKEMON has switched in (if there was an old Pokémon at that position, it is switched out).
* POKEMON|DETAILS represents all the information that can be used to tell Pokémon apart.
* The switched Pokémon has HP health points and STATUS status.
* @author Deltric
* @since January 22, 2022
*/
class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: BattleActor, val publicMessage: BattleMessage, val privateMessage: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val (pnx, _) = publicMessage.pnxAndUuid(0) ?: return
val (actor, activePokemon) = battle.getActorAndActiveSlotFromPNX(pnx)
val entity = if (actor is EntityBackedBattleActor<*>) actor.entity else null
val illusion = publicMessage.battlePokemonFromOptional(battle, "is")
val pokemon = publicMessage.battlePokemon(0, battle) ?: return
if (!battle.started) {
activePokemon.battlePokemon = pokemon
activePokemon.illusion = illusion
val pokemonEntity = pokemon.entity
if (pokemonEntity == null && entity != null) {
val targetPos = battleActor.getSide().getOppositeSide().actors.filterIsInstance<EntityBackedBattleActor<*>>().firstOrNull()?.entity?.pos?.let { pos ->
val offset = pos.subtract(entity.pos)
val idealPos = entity.pos.add(offset.multiply(0.33))
idealPos
} ?: entity.pos
actor.stillSendingOutCount++
pokemon.effectedPokemon.sendOutWithAnimation(
source = entity,
battleId = battle.battleId,
level = entity.world as ServerWorld,
doCry = false,
position = targetPos,
illusion = illusion?.let { IllusionEffect(it.effectedPokemon) }
).thenApply {
actor.stillSendingOutCount--
}
}
else if (pokemonEntity != null) {
illusion?.let { IllusionEffect(it.effectedPokemon).start(pokemonEntity) }
}
} else {
battle.dispatchInsert {
pokemon.sendUpdate()
if (activePokemon.battlePokemon == pokemon) {
return@dispatchInsert emptySet() // Already switched in, Showdown does this if the pokemon is going to die before it can switch
}
activePokemon.battlePokemon?.let { oldPokemon ->
if (publicMessage.effect()?.id == "batonpass") oldPokemon.contextManager.swap(pokemon.contextManager, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
oldPokemon.contextManager.clear(BattleContext.Type.VOLATILE, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
battle.majorBattleActions[oldPokemon.uuid] = publicMessage
}
battle.majorBattleActions[pokemon.uuid] = publicMessage
setOf(
BattleDispatch {
if (entity != null) {
createEntitySwitch(battle, actor, entity, pnx, activePokemon, pokemon, illusion)
} else {
createNonEntitySwitch(battle, actor, pnx, activePokemon, pokemon, illusion)
}
}
)
}
}
}
companion object{
fun createEntitySwitch(battle: PokemonBattle, actor: BattleActor, entity: LivingEntity, pnx: String, activePokemon: ActiveBattlePokemon, newPokemon: BattlePokemon, illusion: BattlePokemon? = null): DispatchResult {
val pokemonEntity = activePokemon.battlePokemon?.entity
// If we can't find the entity for some reason then we're going to skip the recall animation
val sendOutFuture = CompletableFuture<Unit>()
(pokemonEntity?.recallWithAnimation() ?: CompletableFuture.completedFuture(Unit)).thenApply {
// Queue actual swap and send-in after the animation has ended
actor.pokemonList.swap(actor.activePokemon.indexOf(activePokemon), actor.pokemonList.indexOf(newPokemon))
activePokemon.battlePokemon = newPokemon
activePokemon.illusion = illusion
battle.sendSidedUpdate(actor, BattleSwitchPokemonPacket(pnx, newPokemon, true, illusion), BattleSwitchPokemonPacket(pnx, newPokemon, false, illusion))
if (newPokemon.entity != null) {
illusion?.let { IllusionEffect(it.effectedPokemon).start(newPokemon.entity!!) }
newPokemon.entity?.cry()
sendOutFuture.complete(Unit)
} else {
val lastPosition = activePokemon.position
// Send out at previous Pokémon's location if it is known, otherwise actor location
val world = lastPosition?.first ?: entity.world as ServerWorld
val pos = lastPosition?.second ?: entity.pos
newPokemon.effectedPokemon.sendOutWithAnimation(
source = entity,
battleId = battle.battleId,
level = world,
position = pos,
illusion = illusion?.let { IllusionEffect(it.effectedPokemon) }
).thenAccept { sendOutFuture.complete(Unit) }
}
}
return UntilDispatch { sendOutFuture.isDone }
}
fun createNonEntitySwitch(battle: PokemonBattle, actor: BattleActor, pnx: String, activePokemon: ActiveBattlePokemon, newPokemon: BattlePokemon, illusion: BattlePokemon? = null): DispatchResult {
actor.pokemonList.swap(actor.activePokemon.indexOf(activePokemon), actor.pokemonList.indexOf(newPokemon))
activePokemon.battlePokemon = newPokemon
activePokemon.illusion = illusion
battle.sendSidedUpdate(actor, BattleSwitchPokemonPacket(pnx, newPokemon, true, illusion), BattleSwitchPokemonPacket(pnx, newPokemon, false, illusion))
return WaitDispatch(1.5F)
}
}
}
@@ -0,0 +1,28 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-terastallize|POKEMON|TYPE
*
* POKEMON terastallized into TYPE.
* @author Segfault Guy
* @since September 10, 2023
*/
class TerastallizeInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val battlePokemon = message.battlePokemon(0, battle) ?: return
val pokemonName = battlePokemon.getName()
val type = message.effectAt(1)?.let { ElementalTypes.get(it.id) } ?: return
battle.dispatchWaiting {
battle.broadcastChatMessage(battleLang("terastallize", pokemonName, type.displayName).yellow())
battle.minorBattleActions[battlePokemon.uuid] = message
}
}
}
@@ -0,0 +1,32 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.entity.pokemon.effects.TransformEffect
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-transform|POKEMON|POKEMON
*
* POKEMON used Transform to turn into target POKEMON.
* @author jeffw773
* @since November 28, 2023
*/
class TransformInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val pokemonName = pokemon.getName()
val targetPokemon = message.battlePokemon(1, battle) ?: return@dispatchWaiting
val targetPokemonName = targetPokemon.getName()
pokemon.entity?.let { TransformEffect(targetPokemon.effectedPokemon).start(it) }
val lang = battleLang("transform", pokemonName, targetPokemonName)
battle.broadcastChatMessage(lang)
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -24,13 +24,17 @@ import com.cobblemon.mod.common.net.messages.client.battle.BattleSetTeamPokemonP
import com.cobblemon.mod.common.util.battleLang
/**
* Format:
* |turn|NUMBER
* Format: |turn|NUMBER
*
* It is now turn NUMBER.
* @author Deltric
* @since January 22, 2022
*/
class TurnInstruction(val message: BattleMessage) : InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
// TODO maybe tell the client that the turn number has changed
val turnNumber = message.argumentAt(0)?.toInt() ?: return
if (!battle.started) {
battle.started = true
battle.actors.filterIsInstance<PlayerBattleActor>().forEach { actor ->
@@ -54,9 +58,6 @@ class TurnInstruction(val message: BattleMessage) : InterpreterInstruction {
}
}
// TODO maybe tell the client that the turn number has changed
val turnNumber = message.argumentAt(0)?.toInt() ?: return
battle.dispatch {
battle.sendToActors(BattleMakeChoicePacket())
battle.broadcastChatMessage(battleLang("turn", turnNumber).aqua())
@@ -12,7 +12,14 @@ import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.ShowdownInterpreter
/**
* Catch-all for unimplemented instructions that need to be added to the [ShowdownInterpreter].
*
* @author Hiroku
* @since December 25, 2023
*/
class UnknownInstruction(val battleMessage: BattleMessage) : InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo { battle.broadcastChatMessage(battleMessage.rawMessage.red()) }
@@ -13,10 +13,11 @@ import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
/**
* Format:
* |upkeep|
* Format: |upkeep|
*
* Signals the upkeep phase of the turn where the number of turns left for field conditions are updated.
* @author Hiroku
* @since March 12, 2022
*/
class UpkeepInstruction : InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
@@ -0,0 +1,44 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
import net.minecraft.text.Text
/**
* Format: |-weather|WEATHER
*
* Indicates WEATHER is currently in effect.
*
* If upkeep is present, it means that WEATHER was active previously and is still in effect that turn.
* Otherwise, it means that the weather has changed due to a move or ability, or has expired, in which case WEATHER will be none.
* @author Hunter
* @since September 25, 2022
*/
class WeatherInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val weather = message.effectAt(0)?.id ?: return
val user = message.battlePokemonFromOptional(battle)?.getName() ?: Text.literal("UNKNOWN")
ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), user)
battle.dispatchWaiting(1.5F) {
val lang = when {
message.hasOptionalArgument("upkeep") -> battleLang("weather.$weather.upkeep")
weather != "none" -> {
battle.contextManager.add(ShowdownInterpreter.getContextFromAction(message, BattleContext.Type.WEATHER, battle))
battleLang("weather.$weather.start")
}
else -> {
val oldWeather = battle.contextManager.get(BattleContext.Type.WEATHER)?.iterator()?.next()?.id ?: return@dispatchWaiting
battle.contextManager.clear(BattleContext.Type.WEATHER)
battleLang("weather.$oldWeather.end")
}
}
battle.broadcastChatMessage(lang)
}
}
}
@@ -0,0 +1,55 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
import com.cobblemon.mod.common.api.text.gold
import com.cobblemon.mod.common.api.text.plus
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.util.battleLang
import java.util.UUID
/**
* Format: |win|USER
*
* USER has won the battle.
* @author Deltric
* @since January 22, 2022
*/
class WinInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
val user = message.argumentAt(0) ?: return@dispatchGo
val ids = user.split("&").map { it.trim() }
val winners = ids.map { battle.getActor(UUID.fromString(it))!! }
val losers = battle.actors.filter { !winners.contains(it) }
val winnersText = winners.map { it.getName() }.reduce { acc, next -> acc + " & " + next }
battle.broadcastChatMessage(battleLang("win", winnersText).gold())
battle.end()
val wasCaught = battle.showdownMessages.any { "capture" in it }
// If the battle was a PvW battle, we need to set the killer of the wild Pokémon to the player
if (battle.isPvW) {
val nonPlayerActor = battle.actors.first { it.type == ActorType.WILD }
val wildPokemon: BattlePokemon = nonPlayerActor.pokemonList.first()
if (!wasCaught && losers.any { it.uuid == wildPokemon.uuid }) {
wildPokemon.effectedPokemon.entity?.killer = (battle.actors.firstOrNull { it.type == ActorType.PLAYER } as? PlayerBattleActor)?.entity
}
}
CobblemonEvents.BATTLE_VICTORY.post(BattleVictoryEvent(battle, winners, losers, wasCaught))
ShowdownInterpreter.lastCauser.remove(battle.battleId)
}
}
}
@@ -0,0 +1,26 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-zbroken|POKEMON
*
* A z-move has broken through protect and hit POKEMON.
* @author Segfault Guy
* @since September 10, 2023
*/
class ZBrokenInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val battlePokemon = message.battlePokemon(0, battle) ?: return
val pokemonName = battlePokemon.getName()
battle.dispatchWaiting {
battle.broadcastChatMessage(battleLang("zbroken", pokemonName).red())
battle.minorBattleActions[battlePokemon.uuid] = message
}
}
}
@@ -0,0 +1,26 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-zpower|POKEMON
*
* POKEMON has used the z-move variant of its move.
* @author Segfault Guy
* @since September 10, 2023
*/
class ZPowerInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val battlePokemon = message.battlePokemon(0, battle) ?: return
val pokemonName = battlePokemon.getName()
battle.dispatchWaiting {
battle.broadcastChatMessage(battleLang("zpower", pokemonName).yellow())
battle.minorBattleActions[battlePokemon.uuid] = message
}
}
}
@@ -38,6 +38,14 @@ open class BattlePokemon(
effectedPokemon = pokemon.clone(),
postBattleEntityOperation = { entity -> entity.discard() }
)
fun playerOwned(pokemon: Pokemon): BattlePokemon = BattlePokemon(
originalPokemon = pokemon,
effectedPokemon = pokemon,
postBattleEntityOperation = { entity ->
entity.effects.wipe()
}
)
}
val uuid: UUID
@@ -72,10 +80,11 @@ open class BattlePokemon(
val contextManager = ContextManager()
open fun getName(): MutableText {
val displayPokemon = getIllusion()?.effectedPokemon ?: effectedPokemon
return if (actor is PokemonBattleActor || actor is MultiPokemonBattleActor) {
effectedPokemon.getDisplayName()
displayPokemon.getDisplayName()
} else {
battleLang("owned_pokemon", actor.getName(), effectedPokemon.getDisplayName())
battleLang("owned_pokemon", actor.getName(), displayPokemon.getDisplayName())
}
}
@@ -94,4 +103,6 @@ open class BattlePokemon(
fun writeVariables(struct: VariableStruct) {
effectedPokemon.writeVariables(struct)
}
fun getIllusion(): BattlePokemon? = this.actor.activePokemon.find { it.battlePokemon == this }?.illusion
}
@@ -11,7 +11,6 @@ package com.cobblemon.mod.common.client.entity
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.entity.PokemonSideDelegate
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
@@ -22,7 +21,6 @@ import com.cobblemon.mod.common.api.scheduling.lerpOnClient
import com.cobblemon.mod.common.client.particle.BedrockParticleEffectRepository
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.client.render.MatrixWrapper
import com.cobblemon.mod.common.client.render.SnowstormParticle
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PrimaryAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.StatefulAnimation
@@ -34,11 +32,8 @@ import com.cobblemon.mod.common.util.MovingSoundInstance
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.MinecraftClient
import net.minecraft.client.util.math.MatrixStack
import java.lang.Float.min
import kotlin.math.abs
import net.minecraft.entity.Entity
import net.minecraft.entity.data.TrackedData
import net.minecraft.particle.ParticleTypes
import net.minecraft.sound.SoundCategory
import net.minecraft.sound.SoundEvent
import net.minecraft.util.Hand
@@ -45,7 +45,7 @@ object SpawnAllPokemon {
for (species in PokemonSpecies.implemented) {
if (species.nationalPokedexNumber in range) {
LOGGER.debug(species.name)
species.create().sendOut(player.world as ServerWorld, player.pos)
species.create().sendOut(player.world as ServerWorld, player.pos, null)
}
}
@@ -20,6 +20,7 @@ import com.cobblemon.mod.common.api.events.pokemon.ShoulderMountEvent
import com.cobblemon.mod.common.api.interaction.PokemonEntityInteraction
import com.cobblemon.mod.common.api.net.serializers.PoseTypeDataSerializer
import com.cobblemon.mod.common.api.net.serializers.StringSetDataSerializer
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.feature.FlagSpeciesFeature
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.api.reactive.ObservableSubscription
@@ -41,6 +42,8 @@ import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonNavigation
import com.cobblemon.mod.common.entity.pokemon.ai.goals.*
import com.cobblemon.mod.common.entity.pokemon.effects.EffectTracker
import com.cobblemon.mod.common.entity.pokemon.effects.IllusionEffect
import com.cobblemon.mod.common.net.messages.client.animation.PlayPoseableAnimationPacket
import com.cobblemon.mod.common.net.messages.client.sound.UnvalidatedPlaySoundS2CPacket
import com.cobblemon.mod.common.net.messages.client.spawn.SpawnPokemonPacket
@@ -209,9 +212,16 @@ open class PokemonEntity(
PokemonServerDelegate()
}
/** The effects that are modifying this entity. */
var effects: EffectTracker = EffectTracker(this)
/** The species exposed to the client and used on entity spawn. */
val exposedSpecies: Species get() = this.effects.mockEffect?.exposedSpecies ?: this.pokemon.species
/** The form exposed to the client and used for calculating hitbox and height. */
val exposedForm: FormData get() = this.effects.mockEffect?.exposedForm ?: this.pokemon.form
init {
dataTracker.set(SPECIES, pokemon.species.resourceIdentifier.toString())
dataTracker.set(NICKNAME, pokemon.nickname ?: Text.empty())
delegate.initialize(this)
delegate.changePokemon(pokemon)
calculateDimensions()
@@ -239,6 +249,11 @@ open class PokemonEntity(
override fun onTrackedDataSet(data: TrackedData<*>) {
super.onTrackedDataSet(data)
// "But it's imposs-" shut up nerd, it happens during super construction and that's before delegate is assigned by class construction
if (delegate != null) {
delegate.onTrackedDataSet(data)
}
// common datatracker handling
when (data) {
SPECIES -> calculateDimensions()
POSE_TYPE -> {
@@ -258,10 +273,6 @@ open class PokemonEntity(
}
}
}
// "But it's imposs-" shut up nerd, it happens during super construction and that's before delegate is assigned by class construction
if (delegate != null) {
delegate.onTrackedDataSet(data)
}
}
override fun canWalkOnFluid(state: FluidState): Boolean {
@@ -425,6 +436,9 @@ open class PokemonEntity(
nbt.putBoolean(DataKeys.POKEMON_COUNTS_TOWARDS_SPAWN_CAP, false)
}
// save active effects
nbt.put(DataKeys.ENTITY_EFFECTS, effects.saveToNbt())
CobblemonEvents.POKEMON_ENTITY_SAVE.post(PokemonEntitySaveEvent(this, nbt))
return super.writeNbt(nbt)
@@ -466,9 +480,7 @@ open class PokemonEntity(
Pokemon()
}
}
dataTracker.set(SPECIES, pokemon.species.resourceIdentifier.toString())
dataTracker.set(NICKNAME, pokemon.nickname ?: Text.empty())
dataTracker.set(LABEL_LEVEL, pokemon.level)
val savedBattleId = if (nbt.containsUuid(DataKeys.POKEMON_BATTLE_ID)) nbt.getUuid(DataKeys.POKEMON_BATTLE_ID) else null
if (savedBattleId != null) {
val battle = BattleRegistry.getBattle(savedBattleId)
@@ -476,6 +488,14 @@ open class PokemonEntity(
battleId = savedBattleId
}
}
// apply active effects
if (nbt.contains(DataKeys.ENTITY_EFFECTS)) effects.loadFromNBT(nbt.getCompound(DataKeys.ENTITY_EFFECTS))
// init dataTracker
dataTracker.set(SPECIES, effects.mockEffect?.mock?.species ?: pokemon.species.resourceIdentifier.toString())
dataTracker.set(NICKNAME, pokemon.nickname ?: Text.empty())
dataTracker.set(LABEL_LEVEL, pokemon.level)
dataTracker.set(POSE_TYPE, PoseType.valueOf(nbt.getString(DataKeys.POKEMON_POSE_TYPE)))
dataTracker.set(BEHAVIOUR_FLAGS, nbt.getByte(DataKeys.POKEMON_BEHAVIOUR_FLAGS))
@@ -679,13 +699,14 @@ open class PokemonEntity(
}
override fun getDimensions(pose: EntityPose): EntityDimensions {
val scale = pokemon.form.baseScale * pokemon.scaleModifier
return pokemon.form.hitbox.scaled(scale)
val scale = effects.mockEffect?.scale ?: (form.baseScale * pokemon.scaleModifier)
return this.exposedForm.hitbox.scaled(scale)
}
override fun canTakeDamage() = super.canTakeDamage() && !isBusy
override fun damage(source: DamageSource?, amount: Float): Boolean {
return if (super.damage(source, amount)) {
effects.mockEffect?.takeIf { it is IllusionEffect && this.battleId == null }?.end(this)
if (this.health == 0F) {
pokemon.currentHealth = 0
} else {
@@ -710,7 +731,7 @@ open class PokemonEntity(
}
}
override fun getEyeHeight(pose: EntityPose): Float = this.pokemon.form.eyeHeight(this)
override fun getEyeHeight(pose: EntityPose): Float = this.exposedForm.eyeHeight(this)
@Suppress("SENSELESS_COMPARISON")
override fun getActiveEyeHeight(pose: EntityPose, dimensions: EntityDimensions): Float {
@@ -720,7 +741,7 @@ open class PokemonEntity(
if (this.pokemon == null) {
return super.getActiveEyeHeight(pose, dimensions)
}
return this.pokemon.form.eyeHeight(this)
return this.exposedForm.eyeHeight(this)
}
fun setBehaviourFlag(flag: PokemonBehaviourFlag, on: Boolean) {
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.entity.pokemon
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.entity.PokemonSideDelegate
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.battles.BattleRegistry
@@ -36,6 +37,10 @@ class PokemonServerDelegate : PokemonSideDelegate {
lateinit var entity: PokemonEntity
var acknowledgedHPStat = -1
/** Mocked properties exposed to the client [PokemonEntity]. */
private val mock: PokemonProperties?
get() = entity.effects.mockEffect?.mock
override fun changePokemon(pokemon: Pokemon) {
updatePathfindingPenalties(pokemon)
entity.initGoals()
@@ -157,11 +162,15 @@ class PokemonServerDelegate : PokemonSideDelegate {
entity.remove(Entity.RemovalReason.DISCARDED)
}
entity.dataTracker.set(PokemonEntity.SPECIES, entity.pokemon.species.resourceIdentifier.toString())
if (entity.dataTracker.get(PokemonEntity.NICKNAME) != entity.pokemon.nickname) {
entity.dataTracker.set(PokemonEntity.NICKNAME, entity.pokemon.nickname ?: Text.empty())
val trackedSpecies = mock?.species ?: entity.pokemon.species.resourceIdentifier.toString()
val trackedNickname = mock?.nickname ?: entity.pokemon.nickname ?: Text.empty()
val trackedAspects = mock?.aspects ?: entity.pokemon.aspects
entity.dataTracker.set(PokemonEntity.SPECIES, trackedSpecies)
if (entity.dataTracker.get(PokemonEntity.NICKNAME) != trackedNickname) {
entity.dataTracker.set(PokemonEntity.NICKNAME, trackedNickname)
}
entity.dataTracker.set(PokemonEntity.ASPECTS, entity.pokemon.aspects)
entity.dataTracker.set(PokemonEntity.ASPECTS, trackedAspects)
entity.dataTracker.set(PokemonEntity.LABEL_LEVEL, entity.pokemon.level)
entity.dataTracker.set(PokemonEntity.MOVING, !entity.navigation.isIdle)
@@ -197,6 +206,16 @@ class PokemonServerDelegate : PokemonSideDelegate {
}
override fun updatePostDeath() {
// clear active effects before proceeding
if (entity.deathTime == 0) {
entity.effects.wipe()
entity.deathTime = 1
return
}
else if (entity.effects.progress?.isDone == false) {
return
}
entity.dataTracker.set(PokemonEntity.DYING_EFFECTS_STARTED, true)
++entity.deathTime
@@ -0,0 +1,56 @@
package com.cobblemon.mod.common.entity.pokemon.effects
import com.cobblemon.mod.common.api.entity.pokemon.EntityEffect
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import java.util.concurrent.CompletableFuture
/**
* An [EntityEffect] that's applied due to a battle-related condition.
*
* @author Segfault Guy
* @since March 5th, 2024
*/
abstract class BattleEffect : EntityEffect {
open val battleOnly = false
override fun start(entity: PokemonEntity): CompletableFuture<PokemonEntity>? {
val progress = entity.effects.progress?.isDone
if (progress == true || progress == null) {
val future = CompletableFuture<PokemonEntity>()
entity.effects.progress = future
apply(entity, future)
return future
}
else {
return null
}
}
override fun end(entity: PokemonEntity): CompletableFuture<PokemonEntity>? {
val progress = entity.effects.progress?.isDone
if (progress == true || progress == null) {
val future = CompletableFuture<PokemonEntity>()
entity.effects.progress = future
revert(entity, future)
return future
}
else {
return null
}
}
/**
* Applies the effect to the provided [PokemonEntity].
*
* @param future The future to complete once the effect is applied.
*/
protected abstract fun apply(entity: PokemonEntity, future: CompletableFuture<PokemonEntity>)
/**
* Reverts the effect from the provided [PokemonEntity].
*
* @param future The future to complete once the effect is reverted.
*/
protected abstract fun revert(entity: PokemonEntity, future: CompletableFuture<PokemonEntity>)
}
@@ -0,0 +1,46 @@
package com.cobblemon.mod.common.entity.pokemon.effects
import com.cobblemon.mod.common.api.entity.pokemon.EntityEffect
import com.cobblemon.mod.common.api.entity.pokemon.MocKEffect
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.DataKeys
import net.minecraft.nbt.NbtCompound
import java.util.concurrent.CompletableFuture
/**
* The [EntityEffect]s that are active for a [PokemonEntity].
*
* @author Segfault Guy
* @since March 10th, 2024
*/
class EffectTracker(val entity: PokemonEntity) {
/** The progression of the latest [EntityEffect] applied to an entity. */
var progress: CompletableFuture<PokemonEntity>? = null
/** When present, alters the client-side appearance of the [PokemonEntity]. */
var mockEffect: MocKEffect? = null
/** Clears all active [EntityEffect]s gracefully. */
fun wipe(): CompletableFuture<PokemonEntity>? {
return mockEffect?.end(entity)
}
/** Abruptly clears all active [EntityEffect]s. */
fun forceWipe() {
mockEffect = null
}
fun saveToNbt(): NbtCompound {
val nbt = NbtCompound()
mockEffect?.let { effect -> nbt.put(DataKeys.ENTITY_EFFECT_MOCK, effect.saveToNbt()) }
return nbt
}
fun loadFromNBT(nbt: NbtCompound) {
if (nbt.contains(DataKeys.ENTITY_EFFECT_MOCK)) {
val mockTag = nbt.getCompound(DataKeys.ENTITY_EFFECT_MOCK)
this.mockEffect = EntityEffect.loadFromNbt(mockTag)?.takeIf { it is MocKEffect }?.let { it as MocKEffect }
}
}
}
@@ -0,0 +1,59 @@
package com.cobblemon.mod.common.entity.pokemon.effects
import com.cobblemon.mod.common.api.entity.pokemon.*
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonPropertyExtractor
import com.cobblemon.mod.common.api.scheduling.afterOnServer
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.DataKeys
import net.minecraft.nbt.NbtCompound
import java.util.concurrent.CompletableFuture
/**
* A [BattleEffect] that alters a [PokemonEntity] to be disguised as a target [Pokemon].
*
* @param disguise The [Pokemon] to use as a disguise.
* @author Segfault Guy
* @since March 5th, 2024
*/
class IllusionEffect(
override var mock: PokemonProperties = PokemonProperties(),
override var scale: Float = 1.0F
) : BattleEffect(), MocKEffect {
constructor(disguise: Pokemon) : this(
mock = disguise.createPokemonProperties(PokemonPropertyExtractor.ILLUSION),
scale = disguise.form.baseScale * disguise.scaleModifier
)
override fun apply(entity: PokemonEntity, future: CompletableFuture<PokemonEntity>) {
entity.effects.mockEffect = this
future.complete(entity)
}
override fun revert(entity: PokemonEntity, future: CompletableFuture<PokemonEntity>) {
entity.effects.mockEffect = null
afterOnServer(seconds = 1.0F) {
entity.cry()
future.complete(entity)
}
}
override fun saveToNbt(): NbtCompound {
val nbt = NbtCompound()
nbt.putString(DataKeys.ENTITY_EFFECT_ID, ID)
nbt.put(DataKeys.POKEMON_ENTITY_MOCK, mock.saveToNBT())
nbt.putFloat(DataKeys.POKEMON_ENTITY_SCALE, scale)
return nbt
}
override fun loadFromNBT(nbt: NbtCompound) {
if (nbt.contains(DataKeys.POKEMON_ENTITY_MOCK)) this.mock = PokemonProperties().loadFromNBT(nbt.getCompound(DataKeys.POKEMON_ENTITY_MOCK))
if (nbt.contains(DataKeys.POKEMON_ENTITY_SCALE)) this.scale = nbt.getFloat(DataKeys.POKEMON_ENTITY_SCALE)
}
companion object {
val ID = "ILLUSION"
}
}
@@ -0,0 +1,60 @@
package com.cobblemon.mod.common.entity.pokemon.effects
import com.cobblemon.mod.common.api.entity.pokemon.MocKEffect
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonPropertyExtractor
import com.cobblemon.mod.common.api.scheduling.afterOnServer
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.DataKeys
import net.minecraft.nbt.NbtCompound
import java.util.concurrent.CompletableFuture
/**
* A [BattleEffect] that alters a [PokemonEntity] to look like a target [Pokemon].
*
* @param mimic The [Pokemon] to copy.
* @author Segfault Guy
* @since March 5th, 2024
*/
class TransformEffect(
override var mock: PokemonProperties = PokemonProperties(),
override var scale: Float = 1.0F
) : BattleEffect(), MocKEffect {
constructor(mimic: Pokemon) : this(
mock = mimic.createPokemonProperties(PokemonPropertyExtractor.TRANSFORM),
scale = mimic.form.baseScale * mimic.scaleModifier
)
override fun apply(entity: PokemonEntity, future: CompletableFuture<PokemonEntity>) {
entity.effects.mockEffect = this
afterOnServer(seconds = 1.0F) {
entity.cry()
future.complete(entity)
}
}
override fun revert(entity: PokemonEntity, future: CompletableFuture<PokemonEntity>) {
entity.effects.mockEffect = null
entity.cry()
future.complete(entity)
}
override fun saveToNbt(): NbtCompound {
val nbt = NbtCompound()
nbt.putString(DataKeys.ENTITY_EFFECT_MOCK, ID)
nbt.put(DataKeys.POKEMON_ENTITY_MOCK, mock.saveToNBT())
nbt.putFloat(DataKeys.POKEMON_ENTITY_SCALE, scale)
return nbt
}
override fun loadFromNBT(nbt: NbtCompound) {
if (nbt.contains(DataKeys.POKEMON_ENTITY_MOCK)) this.mock = PokemonProperties().loadFromNBT(nbt.getCompound(DataKeys.POKEMON_ENTITY_MOCK))
if (nbt.contains(DataKeys.POKEMON_ENTITY_SCALE)) this.scale = nbt.getFloat(DataKeys.POKEMON_ENTITY_SCALE)
}
companion object {
val ID = "TRANSFORM "
}
}
@@ -64,7 +64,7 @@ class BattleInitializePacket() : NetworkPacket<BattleInitializePacket> {
showdownId = actor.showdownId,
displayName = actor.getName(),
activePokemon = actor.activePokemon.map { it.battlePokemon?.let {
pkm -> ActiveBattlePokemonDTO.fromPokemon(pkm, allySide == side)
pkm -> ActiveBattlePokemonDTO.fromPokemon(pkm, allySide == side, illusion = it.illusion)
} },
type = actor.type
)
@@ -156,18 +156,19 @@ class BattleInitializePacket() : NetworkPacket<BattleInitializePacket> {
val statChanges: MutableMap<Stat, Int>
) {
companion object {
fun fromPokemon(battlePokemon: BattlePokemon, isAlly: Boolean): ActiveBattlePokemonDTO {
fun fromPokemon(battlePokemon: BattlePokemon, isAlly: Boolean, illusion: BattlePokemon?): ActiveBattlePokemonDTO {
val pokemon = battlePokemon.effectedPokemon
val disguise = if (isAlly) pokemon else illusion?.effectedPokemon ?: pokemon
val hpValue = if (isAlly) pokemon.currentHealth.toFloat() else pokemon.currentHealth.toFloat() / pokemon.hp
return ActiveBattlePokemonDTO(
uuid = pokemon.uuid,
displayName = pokemon.getDisplayName(),
properties = pokemon.createPokemonProperties(
uuid = disguise.uuid,
displayName = disguise.getDisplayName(),
properties = disguise.createPokemonProperties(
PokemonPropertyExtractor.SPECIES,
PokemonPropertyExtractor.LEVEL,
PokemonPropertyExtractor.GENDER
),
aspects = pokemon.aspects,
aspects = disguise.aspects,
status = pokemon.status?.status,
hpValue = hpValue,
maxHp = pokemon.hp.toFloat(),
@@ -26,7 +26,8 @@ class BattleSwitchPokemonPacket(val pnx: String, val newPokemon: BattleInitializ
override val id = ID
constructor(pnx: String, newPokemon: BattlePokemon, isAlly: Boolean) : this(pnx, BattleInitializePacket.ActiveBattlePokemonDTO.fromPokemon(newPokemon, isAlly), isAlly)
constructor(pnx: String, newPokemon: BattlePokemon, isAlly: Boolean, illusion: BattlePokemon?) :
this(pnx, BattleInitializePacket.ActiveBattlePokemonDTO.fromPokemon(newPokemon, isAlly, illusion), isAlly)
override fun encode(buffer: PacketByteBuf) {
buffer.writeString(pnx)
@@ -47,7 +47,7 @@ class SpawnPokemonPacket(
constructor(entity: PokemonEntity, vanillaSpawnPacket: EntitySpawnS2CPacket) : this(
entity.ownerUuid,
entity.pokemon.scaleModifier,
entity.pokemon.species,
entity.exposedSpecies,
entity.pokemon.form,
entity.pokemon.aspects,
entity.battleId,
@@ -53,6 +53,7 @@ import com.cobblemon.mod.common.api.types.tera.TeraType
import com.cobblemon.mod.common.api.types.tera.TeraTypes
import com.cobblemon.mod.common.config.CobblemonConfig
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.effects.IllusionEffect
import com.cobblemon.mod.common.net.messages.client.PokemonUpdatePacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.*
import com.cobblemon.mod.common.net.serverhandling.storage.SendOutPokemonHandler.SEND_OUT_DURATION
@@ -459,10 +460,11 @@ open class Pokemon : ShowdownIdentifiable {
return this.form.showdownId()
}
fun sendOut(level: ServerWorld, position: Vec3d, mutation: (PokemonEntity) -> Unit = {}): PokemonEntity? {
fun sendOut(level: ServerWorld, position: Vec3d, illusion: IllusionEffect?, mutation: (PokemonEntity) -> Unit = {}): PokemonEntity? {
CobblemonEvents.POKEMON_SENT_PRE.postThen(PokemonSentPreEvent(this, level, position)) {
SeasonFeatureHandler.updateSeason(this, level, position.toBlockPos())
val entity = PokemonEntity(level, this)
illusion?.start(entity)
entity.setPositionSafely(position)
mutation(entity)
level.spawnEntity(entity)
@@ -478,16 +480,17 @@ open class Pokemon : ShowdownIdentifiable {
position: Vec3d,
battleId: UUID? = null,
doCry: Boolean = true,
illusion: IllusionEffect? = null,
mutation: (PokemonEntity) -> Unit = {},
): CompletableFuture<PokemonEntity> {
// Handle special case of shouldered Cobblemon
if (this.state is ShoulderedState) {
return sendOutFromShoulder(source as ServerPlayerEntity, level, position, battleId, doCry, mutation)
return sendOutFromShoulder(source as ServerPlayerEntity, level, position, battleId, doCry, illusion, mutation)
}
// Proceed as normal for non-shouldered Cobblemon
val future = CompletableFuture<PokemonEntity>()
sendOut(level, position) {
sendOut(level, position, illusion) {
getOwnerPlayer()?.let{
it.swingHand(Hand.MAIN_HAND, true)
level.playSoundServer(it.pos, CobblemonSounds.POKE_BALL_THROW, volume = 0.6F)
@@ -520,6 +523,7 @@ open class Pokemon : ShowdownIdentifiable {
targetPosition: Vec3d,
battleId: UUID? = null,
doCry: Boolean = true,
illusion: IllusionEffect? = null,
mutation: (PokemonEntity) -> Unit = {}
): CompletableFuture<PokemonEntity> {
val future = CompletableFuture<PokemonEntity>()
@@ -534,7 +538,7 @@ open class Pokemon : ShowdownIdentifiable {
val rotatedOffset = Vec3d(shoulderHorizontalOffset, approxShoulderMonHight, 0.0).rotateY(-rotation * (Math.PI.toFloat() / 180f))
val currentPosition = player.pos.add(rotatedOffset)
sendOut(level, currentPosition) {
sendOut(level, currentPosition, illusion) {
// Play some sound indicating hopping off
level.playSoundServer(currentPosition, CobblemonSounds.PC_DROP, volume = 0.6F)
@@ -90,7 +90,7 @@ object CobblemonHeldItemManager : BaseCobblemonHeldItemManager() {
battle.broadcastChatMessage(battleLang("item.$itemID.start", battlerName))
return
}
val sourceName = battleMessage.getSourceBattlePokemon(battle)?.getName() ?: Text.of("UNKNOWN")
val sourceName = battleMessage.battlePokemonFromOptional(battle)?.getName() ?: Text.of("UNKNOWN")
val itemName = this.nameOf(itemID)
val effectId = effect.id
val text = when (effectId) {
@@ -126,7 +126,7 @@ object CobblemonHeldItemManager : BaseCobblemonHeldItemManager() {
this.take(pokemon, itemID)
return
}
val sourceName = battleMessage.getSourceBattlePokemon(battle)?.getName() ?: Text.of("UNKNOWN")
val sourceName = battleMessage.battlePokemonFromOptional(battle)?.getName() ?: Text.of("UNKNOWN")
val effect = battleMessage.effect()
val text = when {
effect?.id != null -> battleLang("enditem.${effect.id}", battlerName, itemName, sourceName)
@@ -68,6 +68,14 @@ object DataKeys {
const val POKEMON_ORIGINAL_TRAINER = "PokemonOriginalTrainer"
const val POKEMON_ORIGINAL_TRAINER_TYPE = "PokemonOriginalTrainerType"
// Entity effects
const val ENTITY_EFFECTS = "EntityEffects"
const val ENTITY_EFFECT_MOCK = "EntityEffectMock"
const val ENTITY_EFFECT_ID = "EntityEffectID"
const val POKEMON_ENTITY_MOCK = "PokemonEntityMock"
const val POKEMON_ENTITY_SCALE = "PokemonEntityScale"
// Evolution stuff
const val POKEMON_EVOLUTIONS = "Evolutions"
const val POKEMON_PENDING_EVOLUTIONS = "Pending"
@@ -1299,6 +1299,7 @@
"cobblemon.battle.end.powertrick": "%1$s switched its Attack and Defense!",
"cobblemon.battle.end.healblock": "%1$s's Heal Block wore off!",
"cobblemon.battle.end.skydrop": "%1$s was freed from the Sky Drop!",
"cobblemon.battle.end.illusion": "%1$s's illusion wore off!",
"cobblemon.battle.block.uproar": "But the uproar kept %1$s awake!",