Showdown Form and Species registration rework.

This commit is contained in:
Segfault Guy
2024-12-03 23:10:25 -05:00
committed by Segfault Guy
parent 641f529de3
commit 7fb873dc2c
21 changed files with 698 additions and 161 deletions
@@ -28,6 +28,7 @@ import com.cobblemon.mod.common.api.pokemon.transformation.evolution.PreEvolutio
import com.cobblemon.mod.common.api.pokemon.transformation.requirement.TransformationRequirement
import com.cobblemon.mod.common.api.pokemon.experience.ExperienceGroup
import com.cobblemon.mod.common.api.pokemon.experience.ExperienceGroupAdapter
import com.cobblemon.mod.common.api.pokemon.labels.CobblemonPokemonLabels
import com.cobblemon.mod.common.api.pokemon.moves.Learnset
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
@@ -43,6 +44,9 @@ import com.cobblemon.mod.common.pokemon.SpeciesAdditions
import com.cobblemon.mod.common.pokemon.transformation.predicate.NbtItemPredicate
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
import com.cobblemon.mod.common.pokemon.transformation.adapters.*
import com.cobblemon.mod.common.pokemon.transformation.form.PermanentForm
import com.cobblemon.mod.common.pokemon.transformation.form.StandardForm
import com.cobblemon.mod.common.pokemon.transformation.form.TemporaryForm
import com.cobblemon.mod.common.util.adapters.*
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.common.collect.HashBasedTable
@@ -206,34 +210,18 @@ object PokemonSpecies : JsonDataRegistry<Species> {
}
/**
* The representation of a [Species] and/or [FormData] in the context of showdown.
* The representation of [FormData] in the context of Showdown.
* This is intended as a sort of DTO that can be easily converted between JSON and Java/JS objects.
*
* @param species The [Species] being converted or the base species if the form is not null.
* @param form The [FormData] being converted int o species (Showdown considers them species) will be null when dealing with the base form.
* @param form The [FormData] being converted into a species (Showdown considers them species).
*/
@Suppress("unused")
internal class ShowdownSpecies(species: Species, form: FormData?) {
val num = species.nationalPokedexNumber
val name = if (form != null) "${createShowdownName(species)}-${form.name}" else createShowdownName(species)
val baseSpecies = if (form != null) createShowdownName(species) else this.name
val forme = form?.name
// ToDo baseForme
val otherFormes = if (form == null && species.forms.isNotEmpty()) species.forms.map { "${this.name}-${it.name}" } else emptyList()
val formeOrder = if (form == null && this.otherFormes.isNotEmpty()) arrayListOf(this.name, *this.otherFormes.toTypedArray()) else emptyList()
val abilities: Map<String, String> = mapOf(
"0" to "No Ability",
"1" to "No Ability",
"H" to "No Ability",
"S" to "No Ability"
)
val types = (form?.types ?: species.types).map { it.name.replaceFirstChar(Char::uppercase) }
val preevo: String? = (form?.preEvolution ?: species.preEvolution)?.let { if (it.form == it.species.standardForm) createShowdownName(it.species) else "${createShowdownName(it.species)}-${it.form.name}" }
// For the context of battles the content here doesn't matter whatsoever and due to how PokemonProperties work we can't guarantee an actual specific species is defined.
val evos = if ((form?.evolutions ?: species.evolutions).isEmpty()) emptyList() else arrayListOf("")
val nfe = this.evos.isNotEmpty()
val eggGroups = (form?.eggGroups ?: species.eggGroups).map { it.showdownID }
val gender: String? = when (form?.maleRatio ?: species.maleRatio) {
internal open class ShowdownSpecies(form: FormData) {
val num = form.species.nationalPokedexNumber
val name = form.showdownName()
val types = form.types.map { it.name.replaceFirstChar(Char::uppercase) }
val eggGroups = form.eggGroups.map { it.showdownID }
val gender: String? = when (form.maleRatio) {
0F -> "F"
1F -> "M"
-1F, 1.125F -> "N"
@@ -241,35 +229,66 @@ object PokemonSpecies : JsonDataRegistry<Species> {
}
val genderRatio = if (this.gender == null)
mapOf(
"maleRatio" to (form?.maleRatio ?: species.maleRatio),
"femaleRation" to (1F - (form?.maleRatio ?: species.maleRatio))
"maleRatio" to (form.maleRatio),
"femaleRation" to (1F - (form.maleRatio))
) else null
val baseStats = mapOf(
"hp" to (form?.baseStats?.get(Stats.HP) ?: species.baseStats[Stats.HP] ?: 1),
"atk" to (form?.baseStats?.get(Stats.ATTACK) ?: species.baseStats[Stats.ATTACK] ?: 1),
"def" to (form?.baseStats?.get(Stats.DEFENCE) ?: species.baseStats[Stats.DEFENCE] ?: 1),
"spa" to (form?.baseStats?.get(Stats.SPECIAL_ATTACK) ?: species.baseStats[Stats.SPECIAL_ATTACK] ?: 1),
"spd" to (form?.baseStats?.get(Stats.SPECIAL_DEFENCE) ?: species.baseStats[Stats.SPECIAL_DEFENCE] ?: 1),
"spe" to (form?.baseStats?.get(Stats.SPEED) ?: species.baseStats[Stats.SPEED] ?: 1)
)
val heightm = (form?.height ?: species.height) / 10
val weightkg = (form?.weight ?: species.weight) / 10
val baseStats = Stats.PERMANENT.associateBy({ it.showdownId }, { form.baseStats[it] ?: 1 })
val heightm = form.height / 10
val weightkg = form.weight / 10
// This is ugly, but we already have it hardcoded in the mod anyway
val maxHP = if (species.showdownId() == "shedinja") 1 else null
val canGigantamax: String? = if (form?.gigantamaxMove != null) form.gigantamaxMove.name else null
val cannotDynamax = form?.dynamaxBlocked ?: species.dynamaxBlocked
// ToDo battleOnly
// ToDo changesFrom
val requiredMove = form?.requiredMove
val requiredItem = form?.requiredItem
val requiredItems = form?.requiredItems
val maxHP = if (this.num == 292) 1 else null
val cannotDynamax = form.dynamaxBlocked
}
private fun createShowdownName(species: Species): String {
if (species.resourceIdentifier.namespace == Cobblemon.MODID) {
return species.name
}
return "${species.resourceIdentifier.namespace}:${species.name}"
/**
* The representation of a [PermanentForm] in the context of Showdown.
*
* @param form The [PermanentForm] being converted into a species (Showdown considers them species).
*/
internal open class ShowdownPermanentForm(form: PermanentForm) : ShowdownSpecies(form) {
val forme = if (form !is StandardForm) form.name else null
val baseSpecies = if (form !is StandardForm) form.species.standardForm.showdownName() else null
val preevo = form.preEvolution?.form?.showdownName()
// For the context of battles the content here doesn't matter whatsoever and due to how PokemonProperties work we can't guarantee an actual specific species is defined.
val evos = if (form.evolutions.isEmpty()) emptyList() else arrayListOf("")
val nfe = this.evos.isNotEmpty()
val canGigantamax = form.temporaryForms.firstOrNull { it.gigantamaxMove != null }?.gigantamaxMove
}
/**
* The representation of a [StandardForm] in the context of Showdown.
*
* @param form The [StandardForm] being converted into a species.
*/
internal class ShowdownStandardForm(form: StandardForm) : ShowdownPermanentForm(form) {
val otherFormes = form.flattenForms().map { it.showdownName() }
val formeOrder = if (this.otherFormes.isNotEmpty()) arrayListOf(this.name, *this.otherFormes.toTypedArray()) else null
}
/**
* The representation of a [TemporaryForm] in the context of Showdown.
*
* @param form The [TemporaryForm] being converted into a species (Showdown considers them species).
* @param baseForm The [PermanentForm] that transitions into this [TemporaryForm].
*/
internal class ShowdownTemporaryForm(form: TemporaryForm) : ShowdownSpecies(form) {
val forme = form.name
val baseSpecies = form.species.standardForm.showdownName()
// forms this form can change from during a battle
val battleOnly = if (form.battleOnly) form.showdownParentForm() else null
// forms this form can change from outside of battle (mutually exclusive with above)
val changesFrom = if (!form.battleOnly) form.showdownParentForm() else null
}
/** Accommodate for Showdown bullshit. */
private fun TemporaryForm.showdownParentForm(): Any? {
// megas and primal don't use battleOnly because... fuck you?
if (this.battleOnly && this.labels.any { it == CobblemonPokemonLabels.MEGA || it == CobblemonPokemonLabels.PRIMAL })
return null
// Necrozma-Ultra and Zygarde-Complete are the only forms where parent form != the forms it can change from
else if (this.parentOverrides.isNotEmpty())
return this.species.standardForm.flattenForms().filter { parentOverrides.contains(it.showdownId()) }.map { it.showdownName() }
else
return parentForm?.showdownName()
}
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.battles.runner
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.runner.graal.GraalShowdownService
import com.cobblemon.mod.common.battles.runner.socket.SocketShowdownService
import com.google.gson.JsonArray
import java.util.UUID
@@ -157,12 +157,16 @@ class GraalShowdownService : ShowdownService {
val jsArray = this.context.eval("js", "new Array();")
var index = 0L
PokemonSpecies.species.forEach { species ->
jsArray.setArrayElement(index++, this.gson.toJson(PokemonSpecies.ShowdownSpecies(species, null)))
species.forms.forEach { form ->
if (form != species.standardForm) {
jsArray.setArrayElement(index++, this.gson.toJson(PokemonSpecies.ShowdownSpecies(species, form)))
jsArray.setArrayElement(index++, this.gson.toJson(PokemonSpecies.ShowdownStandardForm(species.standardForm)))
species.permanentForms.forEach { form ->
jsArray.setArrayElement(index++, this.gson.toJson(PokemonSpecies.ShowdownPermanentForm(form)))
form.temporaryForms.forEach { tempForm ->
jsArray.setArrayElement(index++, this.gson.toJson(PokemonSpecies.ShowdownTemporaryForm(tempForm)))
}
}
species.temporaryForms.forEach { tempForm ->
jsArray.setArrayElement(index++, this.gson.toJson(PokemonSpecies.ShowdownTemporaryForm(tempForm)))
}
}
receiveSpeciesDataFn.execute(jsArray)
}
@@ -123,8 +123,8 @@ class SocketShowdownService(val host: String = "localhost", val port: Int = 1846
return gson.fromJson(response, JsonArray::class.java)
}
private fun sendSpeciesData(species: Species, form: FormData?) {
writer.write(">receiveSpeciesData ${gson.toJson(PokemonSpecies.ShowdownSpecies(species, form))}\n")
private fun sendSpeciesData(species: PokemonSpecies.ShowdownSpecies) {
writer.write(">receiveSpeciesData ${gson.toJson(species)}\n")
acknowledge()
}
@@ -137,12 +137,16 @@ class SocketShowdownService(val host: String = "localhost", val port: Int = 1846
writer.write(">resetSpeciesData\n")
acknowledge()
PokemonSpecies.species.forEach { species ->
sendSpeciesData(species, null)
species.forms.forEach { form ->
if (form != species.standardForm) {
sendSpeciesData(species, form)
sendSpeciesData(PokemonSpecies.ShowdownStandardForm(species.standardForm))
species.permanentForms.forEach { form ->
sendSpeciesData(PokemonSpecies.ShowdownPermanentForm(form))
form.temporaryForms.forEach { tempForm ->
sendSpeciesData(PokemonSpecies.ShowdownTemporaryForm(tempForm))
}
}
species.temporaryForms.forEach { form ->
sendSpeciesData(PokemonSpecies.ShowdownTemporaryForm(form))
}
}
}
@@ -46,8 +46,7 @@ object ChangeEyeHeight {
val height = FloatArgumentType.getFloat(context, "height")
applicator.invoke(pkm, height)
pkm.forms.clear()
pkm.forms.add(FormData().also { it.initialize(pkm) })
// TODO refresh form
return Command.SINGLE_SUCCESS
}
}
@@ -45,8 +45,7 @@ object ChangeScaleAndSize {
pkm.baseScale = scale
pkm.hitbox = EntityDimensions.scalable(width, height)
pkm.forms.clear()
pkm.forms.add(FormData().also { it.initialize(pkm) })
// TODO refresh form
return Command.SINGLE_SUCCESS
}
}
@@ -41,8 +41,7 @@ object ChangeWalkSpeed {
val walkSpeed = FloatArgumentType.getFloat(context, "walkSpeed")
pkm.behaviour.moving.walk.walkSpeed = walkSpeed.toString().asExpressionLike()
pkm.forms.clear()
pkm.forms.add(FormData().also { it.initialize(pkm)})
// TODO refresh form
return Command.SINGLE_SUCCESS
}
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.net.messages.client.pokemon.update
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.TemporaryForm
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
@@ -29,10 +30,12 @@ class FormUpdatePacket(pokemon: () -> Pokemon, form: FormData) : SingleUpdatePac
val ID: ResourceLocation = cobblemonResource("packets/form-update")
fun decode(buffer: RegistryFriendlyByteBuf): FormUpdatePacket {
val pokemon = decodePokemon(buffer)
/*
val form = FormData()
form.species = pokemon().species
form.decode(buffer)
return FormUpdatePacket(pokemon, form)
*/
return FormUpdatePacket(pokemon, TemporaryForm()) // TODO fix
}
}
}
@@ -16,15 +16,13 @@ import com.cobblemon.mod.common.api.abilities.CommonAbility
import com.cobblemon.mod.common.api.abilities.PotentialAbility
import com.cobblemon.mod.common.api.data.ShowdownIdentifiable
import com.cobblemon.mod.common.api.drop.DropTable
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.net.Decodable
import com.cobblemon.mod.common.api.net.Encodable
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffect
import com.cobblemon.mod.common.api.pokemon.egg.EggGroup
import com.cobblemon.mod.common.api.pokemon.transformation.evolution.PreEvolution
import com.cobblemon.mod.common.api.pokemon.experience.ExperienceGroup
import com.cobblemon.mod.common.api.pokemon.experience.ExperienceGroups
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeature
import com.cobblemon.mod.common.api.pokemon.moves.Learnset
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.types.ElementalType
@@ -35,15 +33,22 @@ import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
import com.cobblemon.mod.common.pokemon.ai.FormPokemonBehaviour
import com.cobblemon.mod.common.pokemon.lighthing.LightingData
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.cobblemon.mod.common.util.*
import com.google.gson.annotations.SerializedName
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.world.entity.EntityDimensions
import net.minecraft.resources.ResourceLocation
class FormData(
name: String = "Normal",
/**
* Represents an alternate form of a [Pokemon].
*
* These variations include changes to the base [Species]' stats. For strictly visual variations, see [SpeciesFeature].
*
* @author Hiroku
* @since Dec 30th, 2021
*/
abstract class FormData(
name: String = "Standard",
// Internal for the sake of the base stat provider
@SerializedName("baseStats")
internal var _baseStats: MutableMap<Stat, Int>? = null,
@@ -73,16 +78,12 @@ class FormData(
private val _shoulderEffects: MutableList<ShoulderEffect>? = null,
@SerializedName("moves")
private var _moves: Learnset? = null,
@SerializedName("evolutions")
private val _evolutions: MutableSet<Evolution>? = null,
@SerializedName("abilities")
private var _abilities: AbilityPool? = null,
@SerializedName("drops")
private var _drops: DropTable? = null,
@SerializedName("pokedex")
private var _pokedex: MutableList<String>? = null,
@SerializedName("preEvolution")
private val _preEvolution: PreEvolution? = null,
private var standingEyeHeight: Float? = null,
private var swimmingEyeHeight: Float? = null,
private var flyingEyeHeight: Float? = null,
@@ -96,15 +97,6 @@ class FormData(
private var _height: Float? = null,
@SerializedName("weight")
private var _weight: Float? = null,
val requiredMove: String? = null,
val requiredItem: String? = null,
/** For forms that can accept different items (e.g. Arceus-Grass: Meadow Plate or Grassium-Z). */
val requiredItems: List<String>? = null,
/**
* The [MoveTemplate] of the signature attack of the G-Max form.
* This is always null on any form aside G-Max.
*/
val gigantamaxMove: MoveTemplate? = null,
@SerializedName("battleTheme")
private var _battleTheme: ResourceLocation? = null,
@SerializedName("lightingData")
@@ -162,9 +154,6 @@ class FormData(
var aspects = mutableListOf<String>()
val preEvolution: PreEvolution?
get() = _preEvolution ?: species.preEvolution
val behaviour = FormPokemonBehaviour()
val dynamaxBlocked: Boolean
@@ -189,12 +178,23 @@ class FormData(
get() = _labels ?: species.labels
/**
* Contains the evolutions of this form.
* Do not access this property immediately after a species is loaded, it requires all species in the game to be loaded.
* To be aware of this gamestage subscribe to [PokemonSpecies.observable].
* The [FormData] that precedes this form. Null if a parent form does not exist.
*
* For example the parent of Toxtricity's 'Gmax' form can be either its 'Low-Key' or 'Standard' form.
* The parent of Toxtricity's 'Low-Key' form is its 'Standard' form.
* And the parent of Toxtricity's 'Standard' form is null (does not exist).
*/
val evolutions: MutableSet<Evolution>
get() = _evolutions ?: mutableSetOf()
@delegate:Transient
val parentForm: FormData? by lazy { parentFormInitializer }
@delegate:Transient
val species: Species by lazy { speciesInitializer }
@Transient
private var parentFormInitializer: FormData? = null
@Transient
private lateinit var speciesInitializer: Species
val battleTheme: ResourceLocation
get() = _battleTheme ?: species.battleTheme
@@ -218,30 +218,17 @@ class FormData(
else -> this.standingEyeHeight
}
@Transient
lateinit var species: Species
fun initialize(species: Species): FormData {
this.species = species
open fun initialize(species: Species, parent: FormData? = null): FormData {
this.speciesInitializer = species
this.species
this.parentFormInitializer = parent
this.parentForm
this.behaviour.parent = species.behaviour
Cobblemon.statProvider.provide(this)
// These properties are lazy, these need all species to be reloaded but SpeciesAdditions is what will eventually trigger this after all species have been loaded
this.preEvolution?.species
this.preEvolution?.form
this.evolutions.size
this._lightingData?.let { this._lightingData = it.copy(lightLevel = it.lightLevel.coerceIn(0, 15)) }
return this
}
internal fun resolveEvolutionMoves() {
this.evolutions.forEach { evolution ->
if (evolution.learnableMoves.isNotEmpty() && evolution.result.species != null) {
val pokemon = evolution.result.create()
pokemon.form.moves.evolutionMoves += evolution.learnableMoves
}
}
}
override fun equals(other: Any?): Boolean = other is FormData && other.showdownId() == this.showdownId()
override fun hashCode(): Int = this.showdownId().hashCode()
@@ -280,6 +267,7 @@ class FormData(
pb.writeString(ability.template.name)
}
}
//TODO encode/decode Transformation properties of TemporaryForms
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
@@ -324,7 +312,7 @@ class FormData(
*
* @return The literal Showdown ID of this species and form.
*/
override fun showdownId(): String = this.species.showdownId() + this.formOnlyShowdownId()
override fun showdownId(): String = this.parentForm?.let { parent -> parent.showdownId() + this.formOnlyShowdownId() } ?: this.species.showdownId()
/**
* The literal Showdown ID of this form [name].
@@ -335,4 +323,11 @@ class FormData(
*/
fun formOnlyShowdownId(): String = ShowdownIdentifiable.REGEX.replace(this.name.lowercase(), "")
/**
* The Showdown name of this form appended to [Species.showdownName].
* For example form 'Gmax' of parent form 'Low-Key' of species 'Toxtricity' becomes 'Toxtricity-Low-Key-Gmax'.
*
* @return The Showdown name of this species and form.
*/
open fun showdownName(): String = this.parentForm?.let { parent -> parent.showdownName() + "-" + this.name } ?: this.species.showdownName()
}
@@ -101,6 +101,7 @@ import com.cobblemon.mod.common.pokemon.properties.BattleCloneProperty
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.status.PersistentStatus
import com.cobblemon.mod.common.pokemon.status.PersistentStatusContainer
import com.cobblemon.mod.common.pokemon.transformation.form.PermanentForm
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.codec.internal.ClientPokemonP1
@@ -191,7 +192,7 @@ open class Pokemon : ShowdownIdentifiable {
_species.emit(value)
}
var form = species.standardForm
var form: FormData = species.standardForm
set(value) {
val old = field
// Species updates already update HP but just a form change may require it
@@ -515,11 +516,17 @@ open class Pokemon : ShowdownIdentifiable {
val storeCoordinates = SettableObservable<StoreCoordinates<*>?>(null)
// We want non-optional evolutions to trigger first to avoid unnecessary packets and any cost associate with an optional one that would just be lost
val evolutions: Iterable<Evolution> get() = this.form.evolutions.sortedBy { evolution -> evolution.optional }
val evolutions: Iterable<Evolution> get() {
val form = this.form
return if (form is PermanentForm) form.evolutions.sortedBy { evolution -> evolution.optional } else this.species.evolutions
}
val lockedEvolutions: Iterable<Evolution>
get() = evolutions.filter { it !in evolutionProxy.current() }
val preEvolution: PreEvolution? get() = this.form.preEvolution
val preEvolution: PreEvolution? get() {
val form = this.form
return if (form is PermanentForm) form.preEvolution else this.species.preEvolution
}
/**
* Provides the sided [EvolutionController]s, these operations can be done safely with a simple side check.
@@ -22,12 +22,12 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffect
import com.cobblemon.mod.common.api.pokemon.egg.EggGroup
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.transformation.evolution.PreEvolution
import com.cobblemon.mod.common.api.pokemon.experience.ExperienceGroups
import com.cobblemon.mod.common.api.pokemon.moves.Learnset
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.transformation.Transformation
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.entity.PoseType.Companion.FLYING_POSES
@@ -37,6 +37,9 @@ import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
import com.cobblemon.mod.common.pokemon.ai.PokemonBehaviour
import com.cobblemon.mod.common.pokemon.lighthing.LightingData
import com.cobblemon.mod.common.pokemon.transformation.form.PermanentForm
import com.cobblemon.mod.common.pokemon.transformation.form.StandardForm
import com.cobblemon.mod.common.pokemon.transformation.form.TemporaryForm
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.util.codec.CodecUtils
import com.mojang.serialization.Codec
@@ -109,10 +112,23 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
var weight = 1F
private set
var forms = mutableListOf<FormData>()
var permanentForms = mutableListOf<PermanentForm>()
private set
val standardForm by lazy { FormData(_evolutions = this.evolutions).initialize(this) }
var temporaryForms = mutableListOf<TemporaryForm>()
private set
val standardForm by lazy {
StandardForm(
_evolutions = this.evolutions,
_permanentForms = this.permanentForms,
_temporaryForms = this.temporaryForms
).initialize(this) as StandardForm
}
val forms: Collection<FormData> get() = permanentForms + temporaryForms
val transformations: Collection<Transformation> get() = evolutions + temporaryForms
var labels = hashSetOf<String>()
private set
@@ -164,15 +180,13 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
fun initialize() {
Cobblemon.statProvider.provide(this)
this.forms.forEach { it.initialize(this) }
if (this.forms.isNotEmpty() && this.forms.none { it == this.standardForm }) {
this.forms.add(0, this.standardForm)
}
this.lightingData?.let { this.lightingData = it.copy(lightLevel = it.lightLevel.coerceIn(0, 15)) }
// These properties are lazy, these need all species to be reloaded but SpeciesAdditions is what will eventually trigger this after all species have been loaded
this.preEvolution?.species
this.preEvolution?.form
this.evolutions.size
// init all forms
this.standardForm
}
// Ran after initialize due to us creating a Pokémon here which requires all the properties in #initialize to be present for both this and the results, this is the easiest way to quickly resolve species + form
@@ -183,7 +197,7 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
pokemon.form.moves.evolutionMoves += evolution.learnableMoves
}
}
this.forms.forEach(FormData::resolveEvolutionMoves)
this.permanentForms.forEach(PermanentForm::resolveEvolutionMoves)
}
fun create(level: Int = 10) = PokemonProperties.parse("species=\"${this.name}\" level=${level}").create()
@@ -227,8 +241,9 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
// Hitbox end
this.moves.encode(buffer)
buffer.writeCollection(this.pokedex) { pb, line -> pb.writeString(line) }
buffer.writeCollection(this.forms) { _, form -> form.encode(buffer) }
buffer.writeIdentifier(this.battleTheme)
buffer.writeCollection(this.permanentForms) { _, form -> form.encode(buffer) }
buffer.writeCollection(this.temporaryForms) { _, form -> form.encode(buffer) }
buffer.writeCollection(this.features) { pb, feature -> pb.writeString(feature) }
buffer.writeNullable(this.lightingData) { pb, data ->
pb.writeInt(data.lightLevel)
@@ -261,9 +276,11 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
this.moves.decode(buffer)
this.pokedex.clear()
this.pokedex += buffer.readList { pb -> pb.readString() }
this.forms.clear()
this.forms += buffer.readList{ FormData().apply { decode(buffer) } }.filterNotNull()
this.battleTheme = buffer.readIdentifier()
this.permanentForms.clear()
this.permanentForms += buffer.readList{ PermanentForm().apply { decode(buffer) } }.filterNotNull()
this.temporaryForms.clear()
this.temporaryForms += buffer.readList{ TemporaryForm().apply { decode(buffer) } }.filterNotNull()
this.features.clear()
this.features += buffer.readList { pb -> pb.readString() }
this.lightingData = buffer.readNullable { pb -> LightingData(pb.readInt(), pb.readEnumConstant(LightingData.LiquidGlowMode::class.java)) }
@@ -308,7 +325,7 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
/**
* The literal Showdown ID of this species.
* This will be a lowercase version of the [name] with all the non-alphanumeric characters removed. For example, "Mr. Mime" becomes "mrmime".
* If a Species is not a part of the Cobblemon mon the [resourceIdentifier] will have the namespace appended at the start of the ID resulting in something such as 'somemodaspecies'
* If a Species is not a part of the Cobblemon mod, the [resourceIdentifier] will have the namespace appended at the start of the ID resulting in something such as 'somemodaspecies'
*
* @return The literal Showdown ID of this species.
*/
@@ -330,6 +347,19 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
*/
private fun unformattedShowdownId(): String = ShowdownIdentifiable.REGEX.replace(this.name.lowercase(), "")
/**
* The Showdown name of this species.
* If a species is not a part of the Cobblemon mod, the [resourceIdentifier] will have the namespace appended at
* the start of the name. 'Namespace:Species'.
*
* @return The Showdown name of this species.
*/
fun showdownName(): String {
val namespace = this.resourceIdentifier.namespace
val name = this.name
return if (namespace == Cobblemon.MODID) name else "$namespace:$name"
}
override fun toString() = this.showdownId()
companion object {
@@ -0,0 +1,72 @@
package com.cobblemon.mod.common.pokemon.transformation.form
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeature
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.pokemon.transformation.Transformation
import com.cobblemon.mod.common.api.pokemon.transformation.evolution.PreEvolution
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.google.gson.annotations.SerializedName
/**
* A form of a [Species] that is noninterchangeable with the [StandardForm]. These forms behave like a different species,
* and can have a unique set of interchangeable [TemporaryForm]s, but share the same dex number. They are a variation
* determined by [Evolution] or [SpeciesFeature].
*
* For example:
* https://bulbapedia.bulbagarden.net/wiki/Regional_form
* https://bulbapedia.bulbagarden.net/wiki/Lycanroc#Forms
*
* @author Segfault Guy
* @since October 21st, 2023
*/
open class PermanentForm (
@SerializedName("evolutions")
private val _evolutions: MutableSet<Evolution>? = null,
@SerializedName("preEvolution")
private val _preEvolution: PreEvolution? = null,
@SerializedName("temporaryForms")
private val _temporaryForms: MutableList<TemporaryForm>? = null,
) : FormData() {
/**
* Contains the evolutions of this form.
* Do not access this property immediately after a species is loaded, it requires all species in the game to be loaded.
* To be aware of this gamestage subscribe to [PokemonSpecies.observable].
*/
val evolutions: MutableSet<Evolution>
get() = _evolutions ?: mutableSetOf()
val preEvolution: PreEvolution?
get() = _preEvolution ?: species.preEvolution
val temporaryForms: MutableList<TemporaryForm>
get() = _temporaryForms ?: mutableListOf()
open val forms: List<FormData> get() = temporaryForms
open val transformations: List<Transformation> get() = temporaryForms + evolutions
override fun initialize(species: Species, parent: FormData?): FormData {
super.initialize(species, parent)
// These properties are lazy, these need all species to be reloaded but SpeciesAdditions is what will eventually trigger this after all species have been loaded
this.preEvolution?.species
this.preEvolution?.form
this.evolutions.size
// init nested forms
this.forms.forEach { it.initialize(species, this) }
return this
}
internal fun resolveEvolutionMoves() {
this.evolutions.forEach { evolution ->
if (evolution.learnableMoves.isNotEmpty() && evolution.result.species != null) {
val pokemon = evolution.result.create()
pokemon.form.moves.evolutionMoves += evolution.learnableMoves
}
}
}
}
@@ -0,0 +1,30 @@
package com.cobblemon.mod.common.pokemon.transformation.form
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.google.gson.annotations.SerializedName
/**
* The [FormData] representation of a [Species].
*
* @author Segfault Guy
* @since December 10th, 2023
*/
class StandardForm(
@SerializedName("evolutions")
private val _evolutions: MutableSet<Evolution>? = null,
@SerializedName("temporaryForms")
private val _temporaryForms: MutableList<TemporaryForm>? = null,
@SerializedName("regularForms")
private val _permanentForms: MutableList<PermanentForm>? = null,
) : PermanentForm(_evolutions = _evolutions, _temporaryForms = _temporaryForms) {
val permanentForms: MutableList<PermanentForm>
get() = _permanentForms ?: mutableListOf()
override val forms: List<FormData> = this.temporaryForms + this.permanentForms
/** Aggregates child forms and their nested forms. */
fun flattenForms(): List<FormData> = this.forms + this.permanentForms.filter { it != this }.flatMap { it.temporaryForms }
}
@@ -0,0 +1,38 @@
package com.cobblemon.mod.common.pokemon.transformation.form
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.pokemon.transformation.Transformation
import com.cobblemon.mod.common.api.pokemon.transformation.requirement.TransformationRequirement
import com.cobblemon.mod.common.api.pokemon.transformation.trigger.TransformationTrigger
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.pokemon.transformation.triggers.ItemInteractionTrigger
/**
* A form of a [Species] that is interchangeable with a parent [PermanentForm]. These forms are temporary and may be toggled
* OUTSIDE of battle by its [TransformationTrigger] when the [TransformationRequirement]s are satisfied.
*
* For example:
* https://bulbapedia.bulbagarden.net/wiki/Mega_Evolution
* https://bulbapedia.bulbagarden.net/wiki/Rotom#Forms
*
* Note: triggering of forms INSIDE a battle is forced by Showdown.
*
* @author Segfault Guy
* @since October 21st, 2023
*/
class TemporaryForm (
/** The [MoveTemplate] of the signature attack of the G-Max form. This is always null on any form aside G-Max. */
val gigantamaxMove: MoveTemplate? = null,
/** Whether this form is triggered during a battle. */
val battleOnly: Boolean = false,
/**
* When populated by form name(s) in [showdownId] format, overrides the [parentForm] that this form may transform from
* (e.g. Necrozma's 'Ultra' from is special can transform from its 'Dawn-Wings' or 'Dusk-Mane forms').
*/
val parentOverrides: List<String> = emptyList(),
/** The [TransformationTrigger] to attempt this transformation OUTSIDE of battle. */
override val trigger: TransformationTrigger = ItemInteractionTrigger(),
/** The [TransformationRequirement]s that need to be satisfied for this transformation OUTSIDE of battle. */
override val requirements: Set<TransformationRequirement> = mutableSetOf()
) : FormData(), Transformation
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.pokemon.transformation.progress.Transformati
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressType
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressTypes
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.PermanentForm
import com.cobblemon.mod.common.pokemon.transformation.requirements.DamageTakenRequirement
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
@@ -60,8 +61,10 @@ class DamageTakenTransformationProgress : TransformationProgress<DamageTakenTran
}
fun supports(pokemon: Pokemon): Boolean {
return pokemon.form.evolutions.any { evolution ->
evolution.requirements.any { requirement ->
val form = pokemon.form
if (form !is PermanentForm) return false
return form.transformations.any { transformation ->
transformation.requirements.any { requirement ->
requirement is DamageTakenRequirement
}
}
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.pokemon.transformation.progress.Transformati
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressType
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressTypes
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.PermanentForm
import com.cobblemon.mod.common.pokemon.transformation.requirements.DefeatRequirement
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
@@ -43,8 +44,10 @@ class DefeatTransformationProgress : TransformationProgress<DefeatTransformation
}
override fun shouldKeep(pokemon: Pokemon): Boolean {
return pokemon.form.evolutions.any { evolution ->
evolution.requirements.any { requirement ->
val form = pokemon.form
if (form !is PermanentForm) return false
return form.transformations.any { transformation ->
transformation.requirements.any { requirement ->
requirement is DefeatRequirement && requirement.target.originalString.equals(this.progress.target.originalString, true)
}
}
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.pokemon.transformation.progress.Transformati
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressType
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressTypes
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.PermanentForm
import com.cobblemon.mod.common.pokemon.transformation.requirements.BattleCriticalHitsRequirement
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
@@ -54,8 +55,10 @@ class LastBattleCriticalHitsTransformationProgress : TransformationProgress<Last
}
fun supports(pokemon: Pokemon): Boolean {
return pokemon.form.evolutions.any { evolution ->
evolution.requirements.any { requirement ->
val form = pokemon.form
if (form !is PermanentForm) return false
return form.transformations.any { transformation ->
transformation.requirements.any { requirement ->
requirement is BattleCriticalHitsRequirement
}
}
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.pokemon.transformation.progress.Transformati
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressType
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressTypes
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.PermanentForm
import com.cobblemon.mod.common.pokemon.transformation.requirements.RecoilRequirement
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.serialization.Codec
@@ -60,8 +61,10 @@ class RecoilTransformationProgress : TransformationProgress<RecoilTransformation
}
fun supports(pokemon: Pokemon): Boolean {
return pokemon.form.evolutions.any { evolution ->
evolution.requirements.any { requirement ->
val form = pokemon.form
if (form !is PermanentForm) return false
return form.transformations.any { transformation ->
transformation.requirements.any { requirement ->
requirement is RecoilRequirement
}
}
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.pokemon.transformation.progress.Transformati
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressType
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgressTypes
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.PermanentForm
import com.cobblemon.mod.common.pokemon.transformation.requirements.UseMoveRequirement
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
@@ -66,8 +67,10 @@ class UseMoveTransformationProgress : TransformationProgress<UseMoveTransformati
}
fun supports(pokemon: Pokemon, move: MoveTemplate): Boolean {
return pokemon.form.evolutions.any { evolution ->
evolution.requirements.any { requirement ->
val form = pokemon.form
if (form !is PermanentForm) return false
return form.transformations.any { transformation ->
transformation.requirements.any { requirement ->
requirement is UseMoveRequirement && requirement.move == move
}
}
@@ -26,7 +26,8 @@ import net.minecraft.world.item.ItemStack
* @since March 21st, 2022
*/
class HeldItemRequirement(
val itemCondition: NbtItemPredicate = NbtItemPredicate(ItemIdentifierCondition(ResourceLocation.parse("air"))),
val itemCondition: NbtItemPredicate =
NbtItemPredicate(ItemIdentifierCondition(ResourceLocation.fromNamespaceAndPath("minecraft", "air"))),
val consumeHeldItem: Boolean? = null
) : TransformationRequirement {
@@ -5,6 +5,10 @@
"description": "A Pokémon species",
"type": "object",
"properties": {
"implemented": {
"type": "boolean",
"default": false
},
"name": {
"description": "The Pokémon's name",
"type": "string"
@@ -21,9 +25,12 @@
"description": "The Pokémon secondary type",
"type": "string"
},
"maleRatio": {
"description": "The Pokémon's gender ratio",
"type": ["number", "null"]
"abilities": {
"description": "The Pokémon's possible abilities",
"type": "array",
"items": {
"type": "string"
}
},
"baseStats": {
"description": "The Pokémon's base stats",
@@ -57,6 +64,110 @@
"description": "The Pokémon's catch rate",
"type": "integer"
},
"maleRatio": {
"description": "The Pokémon's gender ratio",
"type": "number"
},
"baseFriendship": {
"description": "The Pokémon's base friendship",
"type": "integer"
},
"experienceGroup": {
"description": "The Pokémon's experience group",
"type": "string"
},
"eggCycles": {
"description": "The Pokémon's egg cycles",
"type": "integer"
},
"eggGroups": {
"description": "The Pokémon's egg group",
"type": "string"
},
"height": {
"description": "The Pokémon's Pokédex height in decimeters",
"type": "number"
},
"weight": {
"description": "The Pokémon's Pokédex weight in hectograms",
"type": "number"
},
"labels": {
"description": "The Pokémon's descriptors",
"type": "array",
"items": {
"type": "string"
}
},
"features": {
"description": "The Pokémon's features",
"type": "array",
"items": {
"type": "string"
}
},
"aspects": {
"description": "The Pokémon's aspects",
"type": "array",
"items": {
"type": "string"
}
},
"baseExperienceYield": {
"description": "The Pokémon's base stats",
"type": "object"
},
"evYield": {
"description": "The Pokémon's base stats",
"type": "object",
"hp": {
"description": "The Pokémon's base health stat",
"type": "integer"
},
"attack": {
"description": "The Pokémon's base attack stat",
"type": "integer"
},
"defence": {
"description": "The Pokémon's base defence stat",
"type": "integer"
},
"specialAttack": {
"description": "The Pokémon's base special attack stat",
"type": "integer"
},
"specialDefence": {
"description": "The Pokémon's base special defence stat",
"type": "integer"
},
"speed": {
"description": "The Pokémon's base speed stat",
"type": "integer"
}
},
"drops": {
"description": "The items this Pokémon drops on defeat",
"type": "object",
"amount": {
"description": "The maximum number of items this Pokémon may drop",
"type": "integer"
},
"entries": {
"description": "The different items that this Pokémon may drop",
"type": "array",
"items": {
"type": "object",
"item": {
"description": "The ID of the item that is dropped",
"type": "string"
},
"percentage": {
"description": "The chance that this item is one of the items dropped",
"type": "number"
}
}
}
},
"baseScale": {
"description": "The Pokémon's base scale",
"type": "number"
@@ -73,38 +184,233 @@
"type": "number"
},
"fixed": {
"description": "idk",
"type": "boolean"
"description": "Whether the hitbox is fixed",
"type": "boolean",
"default": false
}
},
"battleTheme": {
"description": "The ID of the SoundEvent that's played as the Pokémon's encounter theme",
"type": "string",
"default": "cobblemon:battle.pvw.default"
},
"dynamaxBlocked": {
"description": "Whether the Pokémon is restricted from dynamaxing",
"type": "boolean",
"default": false
},
"shoulderMountable": {
"description": "The Pokémon's gender ratio",
"type": "boolean",
"default": false
},
"behaviour": {
"description": "The Pokémon's hitbox",
"type": "object",
"moving": {
"type": "object",
"walk": {
"description": "The Pokémon's behaviour when on land",
"type": "object",
"canWalk": {
"description": "Whether the Pokémon can move on land",
"type": "boolean",
"default": true
},
"avoidsLand": {
"description": "Whether the Pokémon seeks the nearest source of water",
"type": "boolean",
"default": false
},
"walkSpeed": {
"description": "The Pokémon's speed while on land",
"type": "number",
"default": 0.35
}
},
"swim": {
"description": "The Pokémon's behaviour when in liquid",
"type": "object",
"avoidsWater": {
"description": "Whether the Pokémon seeks the nearest source of land",
"type": "boolean",
"default": false
},
"hurtByLava": {
"description": "Whether the Pokémon is damaged in lava",
"type": "boolean",
"default": true
},
"canSwimInWater": {
"description": "Whether the Pokémon can swim in water",
"type": "boolean",
"default": true
},
"canSwimInLava": {
"description": "Whether the Pokémon can swim in lava",
"type": "boolean",
"default": true
},
"canBreatheUnderwater": {
"description": "Whether the Pokémon can breathe in water",
"type": "boolean",
"default": false
},
"canBreatheUnderlava": {
"description": "Whether the Pokémon can breathe in lava",
"type": "boolean",
"default": false
},
"canWalkOnWater": {
"description": "Whether the Pokémon can walk on water",
"type": "boolean",
"default": false
},
"canWalkOnLava": {
"description": "Whether the Pokémon can walk on lava",
"type": "boolean",
"default": false
},
"swimSpeed": {
"description": "The Pokémon's speed while in liquid",
"type": "number",
"default": 0.3
}
},
"fly": {
"description": "The Pokémon's behaviour when in air",
"type": "object",
"canFly": {
"description": "Whether the Pokémon can move in ar",
"type": "boolean",
"default": false
},
"flySpeedHorizontal": {
"description": "The Pokémon's speed while in air",
"type": "number",
"default": 0.3
}
},
"canLook": {
"description": "Whether the Pokémon can look at targets",
"type": "boolean",
"default": true
},
"looksAtEntities": {
"description": "Whether the Pokémon can look at other entities",
"type": "boolean",
"default": true
},
"wanderChance": {
"description": "The chance that the Pokémon breaks idle state and wanders",
"type": "integer",
"default": 120
},
"wanderSpeed": {
"description": "The speed that the Pokémon wanders",
"type": "number",
"default": 1.0
}
},
"resting": {
"type": "object",
"canSleep": {
"description": "Whether the Pokémon can enter a sleeping state",
"type": "boolean",
"default": false
},
"times": {
"description": "The time of day that the Pokémon can sleep",
"type": "string",
"enum": ["any", "day", "night", "noon", "midnight", "dawn", "dusk", "twilight", "morning", "afternoon"],
"default": "night"
},
"sleepChance": {
"description": "The chance that the Pokémon enters a sleeping state",
"type": "number",
"default": 0.0016
},
"blocks": {
"description": "The blocks that the Pokémon can sleep on",
"type": "array",
"items": {
"type": "string"
}
},
"biomes": {
"description": "The biome that the Pokémon can sleep in",
"type": "array",
"items": {
"type": "string"
}
},
"light": {
"description": "The light level that the Pokémon can sleep in",
"type": "string",
"default": "0-15"
},
"depth": {
"description": "How deep the Pokémon sleeps",
"type": "string",
"enum": ["comatose", "normal"],
"default": "normal"
},
"willSleepOnBed": {
"description": "Whether the Pokémon can sleep on a bed",
"type": "boolean"
}
},
"idle": {
"type": "object",
"pointsAtSpawn": {
"description": "Whether the Pokémon looks towards world spawn",
"type": "boolean",
"default": false
}
}
},
"moves": {
"description": "The Pokémon's learnset",
"type": "array",
"items": {
"type": "string"
}
},
"preEvolution": {
"description": "The Pokémon's pre-evolved form",
"type": "string"
},
"evolutions": {
"description": "The Pokémon's evolutions",
"type": "array",
"items": {
"type": "object",
"variant": {
"description": "The ID of the event that attempts to start an evolution",
"type": "string",
"enum": ["level_up", "trade", "item_interact"]
},
"id": {
"description": "An unique ID for this evolution, IE pikachu_level",
"description": "A unique ID for this evolution (i.e. nosepass_probopass)",
"type": "string"
},
"result": {
"description": "The PokemonProperties that represents the result of this evolution",
"type": "string"
},
"optional": {
"description": "If this evolution will allow the user to choose when to start it or not",
"type": "boolean"
"learnableMoves": {
"description": "The moves learnt after evolution",
"type": "array",
"items": {
"type": "string"
}
},
"consumeHeldItem": {
"description": "If this evolution will consume the held item",
"type": "boolean"
},
"data": {
"description": "The data that backs this event"
"trigger": {
"type": ["object", "string"],
"variant": {
"description": "The ID of the event that attempts to start this evolution",
"type": "string",
"enum": ["level_up", "trade", "item_interact", "block_click"]
},
"requiredContext": {
"description": "The context required to kick-start this evolution",
"type": "string"
}
},
"requirements": {
"description": "The requirements behind this evolution",
@@ -120,12 +426,27 @@
"description": "The data that backs this requirement"
}
}
},
"optional": {
"description": "If this evolution will allow the user to choose when to start it or not",
"type": "boolean",
"default": true
}
}
},
"battleTheme": {
"description": "The SoundEvent ID for the theme to play when encountering a wild Pokémon of this species",
"type": "string"
"temporaryForms": {
"description": "The interchangeable forms of this Pokémon",
"type": "array",
"items": {
"type": "object"
}
},
"permanentForms": {
"description": "The non-interchangeable forms of this Pokémon",
"type": "array",
"items": {
"type": "object"
}
}
}
}