Compare commits

...
Author SHA1 Message Date
Polymeta b79e73928a add multibattle NPC test command for debugging
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-21 15:35:59 +01:00
Polymeta c89ea759a6 fix invalidActionExceptions if the gimmickMove's targetType is different from it's normal move
good example is earthquake (allAdjacent) vs maxquake (adjacentFoe)

Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-08 17:00:58 +01:00
Polymeta c6479525f9 double oop docs
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-07 16:07:55 +01:00
Polymeta 2a7dababab oop licenses
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-07 15:58:21 +01:00
Polymeta 7ec6e61d4a first approach with resetting forms pre-battle
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-07 15:16:06 +01:00
Polymeta 7735af5c11 fixed itempredicates not working when used in species definition
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-07 11:39:51 +01:00
Polymeta 18d25dcf04 small reshuffle for requirements and introduce keyitem requirement (also add aerodactyl as example)
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-07 11:39:18 +01:00
Polymeta e259dc18a2 fix doubles not properly working with gimmicks
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-07 11:26:30 +01:00
Polymeta 7439436e93 fix trailing comma in schema, and check pokemon entity for registryAccess before doing the rest
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-06 10:49:37 +01:00
Polymeta 1c51d5ca98 prevent shiftbutton from being triggered or clicked when its not actually visible
Signed-off-by: Polymeta <40173118+Polymeta@users.noreply.github.com>
2025-05-06 10:42:59 +01:00
Segfault Guy d94952347f Portrait updates and form effects. 2025-04-30 00:30:06 -04:00
Segfault Guy 7fe6141640 form20 blaze it 2025-04-20 23:41:12 -04:00
Segfault Guy 4b1653ca6c Merge remote-tracking branch 'origin/main' into form_changes 2025-04-20 20:53:54 -04:00
Segfault Guy 7265ab57b9 Apply species schema changes to evolutions. 2025-04-11 01:47:41 -04:00
Segfault Guy 27e3a95b06 cleanup test changes 2025-04-11 01:17:29 -04:00
Segfault Guy 38de3c6267 In-battle form changes and PokemonProperties form completion. 2025-04-09 00:18:10 -04:00
Segfault Guy 376e5ac906 Merge branch 'main' into form_changes 2025-03-18 22:26:00 -04:00
Segfault Guy b225283dcc Toggleable, deferrable, and wild forms. 2025-03-12 23:00:29 -04:00
Segfault Guy b90f852a71 Clear invalid forms on species change. 2025-02-24 20:12:23 -05:00
Segfault Guy fdf7a9703a coalesce temp and perm forms under same property and instead deserialize by custom adapter instead of having unique properties. 2025-02-23 23:21:43 -05:00
Segfault Guy bb41f28342 Merge remote-tracking branch 'origin/main' into form_changes 2025-02-18 22:33:23 -05:00
Segfault Guy c5ee7ebe7a Fix ability registration for forms. 2025-02-11 21:40:35 -05:00
Segfault Guy f6ae10e1d9 It's about f(orm)cking time. 2025-02-03 23:38:53 -05:00
Segfault GuyandSegfault Guy b2070b53fb Changing form... changes form? Wow! 2024-12-03 23:41:17 -05:00
Segfault GuyandSegfault Guy 7fb873dc2c Showdown Form and Species registration rework. 2024-12-03 23:10:25 -05:00
Segfault GuyandSegfault Guy 641f529de3 Better Form and Evolution triggering. 2024-12-03 19:12:41 -05:00
Segfault GuyandSegfault Guy db9fb4fefa Refactor Evolution and add Transformation to expand application of Evolution events and requirements. 2024-12-02 23:13:57 -05:00
661 changed files with 5282 additions and 4118 deletions
@@ -86,9 +86,9 @@ import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.aspects.COSMETIC_SLOT_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.FORM_ASPECTS
import com.cobblemon.mod.common.pokemon.aspects.GENDER_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.pokemon.evolution.variants.BlockClickEvolution
import com.cobblemon.mod.common.pokemon.feature.TagSeasonResolver
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
import com.cobblemon.mod.common.pokemon.properties.AspectPropertyType
@@ -100,6 +100,7 @@ import com.cobblemon.mod.common.pokemon.properties.UnaspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.properties.tags.PokemonFlagProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.pokemon.transformation.triggers.BlockClickTrigger
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
@@ -254,13 +255,8 @@ object Cobblemon {
PlatformEvents.RIGHT_CLICK_BLOCK.subscribe { event ->
val player = event.player
val block = player.level().getBlockState(event.pos).block
player.party().forEach { pokemon ->
pokemon.lockedEvolutions
.filterIsInstance<BlockClickEvolution>()
.forEach { evolution ->
evolution.attemptEvolution(pokemon, BlockClickEvolution.BlockInteractionContext(block, player.level()))
}
}
val context = BlockClickTrigger.Context(block, player.level())
player.party().forEach { pokemon -> pokemon.triggerTransformations(BlockClickTrigger::class.java, context) }
}
PlatformEvents.CHANGE_DIMENSION.subscribe {
@@ -301,6 +297,7 @@ object Cobblemon {
SHINY_ASPECT.register()
GENDER_ASPECT.register()
FORM_ASPECTS.register()
COSMETIC_SLOT_ASPECT.register()
SpeciesFeatures.types["choice"] = ChoiceSpeciesFeatureProvider::class.java
@@ -170,9 +170,6 @@ import com.cobblemon.mod.common.net.messages.server.trade.ChangeTradeAcceptanceP
import com.cobblemon.mod.common.net.messages.server.trade.OfferTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.PerformTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.UpdateTradeOfferPacket
import com.cobblemon.mod.common.net.serverhandling.ChallengeHandler
import com.cobblemon.mod.common.net.serverhandling.ChallengeResponseHandler
import com.cobblemon.mod.common.net.serverhandling.RequestInteractionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.BattleSelectActionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.RemoveSpectatorHandler
import com.cobblemon.mod.common.net.serverhandling.battle.SpectateBattleHandler
@@ -211,6 +208,12 @@ import com.cobblemon.mod.common.net.serverhandling.storage.SwapPCPartyPokemonHan
import com.cobblemon.mod.common.net.serverhandling.storage.party.MovePartyPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.ReleasePCPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.SwapPartyPokemonHandler
import com.cobblemon.mod.common.net.messages.client.pokemon.update.form.AddFormPacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.form.ClearFormsPacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.form.RemoveFormPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.form.AcceptFormPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.form.RevertFormPacket
import com.cobblemon.mod.common.net.serverhandling.*
import com.cobblemon.mod.common.net.serverhandling.storage.pc.*
import com.cobblemon.mod.common.net.serverhandling.trade.AcceptTradeRequestHandler
import com.cobblemon.mod.common.net.serverhandling.trade.CancelTradeHandler
@@ -430,6 +433,11 @@ object CobblemonNetwork {
// Pokédex scanning
list.add(PacketRegisterInfo(ServerConfirmedRegisterPacket.ID, ServerConfirmedRegisterPacket::decode, ServerConfirmedRegisterHandler))
// Form packets
list.add(PacketRegisterInfo(AddFormPacket.ID, AddFormPacket::decode, PokemonUpdatePacketHandler()))
list.add(PacketRegisterInfo(ClearFormsPacket.ID, ClearFormsPacket::decode, PokemonUpdatePacketHandler()))
list.add(PacketRegisterInfo(RemoveFormPacket.ID, RemoveFormPacket::decode, PokemonUpdatePacketHandler()))
// Debug / cheats
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
@@ -533,6 +541,10 @@ object CobblemonNetwork {
// NPC packets
list.add(PacketRegisterInfo(SaveNPCPacket.ID, SaveNPCPacket::decode, SaveNPCHandler))
// Form Packets
list.add(PacketRegisterInfo(AcceptFormPacket.ID, AcceptFormPacket::decode, AcceptFormHandler))
list.add(PacketRegisterInfo(RevertFormPacket.ID, RevertFormPacket::decode, RevertFormHandler))
// Riding packet(s)
list.add(PacketRegisterInfo(ServerboundUpdateOrientationPacket.ID, ServerboundUpdateOrientationPacket::decode, OrientationPacketHandler))
list.add(PacketRegisterInfo(ServerboundUpdateRidingStatePacket.ID, ServerboundUpdateRidingStatePacket::decode, ServerboundUpdateRidingStateHandler))
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.api.abilities
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.data.ShowdownIdentifiable
import com.cobblemon.mod.common.util.codec.CodecUtils
import com.google.gson.JsonObject
import com.mojang.serialization.Codec
@@ -25,7 +26,7 @@ class AbilityTemplate(
var builder: (AbilityTemplate, forced: Boolean, priority: Priority) -> Ability = { template, forced, priority -> Ability(template, forced, priority) },
val displayName: String = "cobblemon.ability.$name",
val description: String = "cobblemon.ability.$name.desc"
) {
) : ShowdownIdentifiable {
/**
* Returns the Ability or if applicable the extension connected to this template
*/
@@ -45,6 +46,8 @@ class AbilityTemplate(
*/
fun create(json: JsonObject) = create().loadFromJSON(json)
override fun showdownId(): String = ShowdownIdentifiable.REGEX.replace(this.name.lowercase(), "")
companion object {
@JvmStatic
@@ -48,9 +48,9 @@ import com.cobblemon.mod.common.entity.npc.NPCEntity
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.pokemon.evolution.progress.DefeatEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.requirements.DefeatRequirement
import com.cobblemon.mod.common.pokemon.transformation.progress.DefeatTransformationProgress
import com.cobblemon.mod.common.pokemon.transformation.progress.LastBattleCriticalHitsTransformationProgress
import com.cobblemon.mod.common.pokemon.transformation.requirements.DefeatRequirement
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.giveOrDropItemStack
@@ -77,7 +77,7 @@ open class PokemonBattle(
val side2: BattleSide
) {
/** Whether logging will be silenced for this battle. */
var mute = true
var mute = false
val struct = this.asMoLangValue()
val runtime = MoLangRuntime().also { it.environment.query = struct }
@@ -92,7 +92,7 @@ open class PokemonBattle(
actor.battle = this
actor.pokemonList.forEach { battlePokemon ->
battlePokemon.effectedPokemon.evolutionProxy.current().progress()
.filterIsInstance<LastBattleCriticalHitsEvolutionProgress>()
.filterIsInstance<LastBattleCriticalHitsTransformationProgress>()
.forEach { it.reset() }
}
}
@@ -252,11 +252,12 @@ open class PokemonBattle(
val facedFainted = opponentPokemon.facedOpponents.contains(faintedPokemon)
val pokemon = opponentPokemon.effectedPokemon
if (facedFainted) {
// TODO refactor and apply to forms
pokemon.lockedEvolutions.forEach { evolution ->
evolution.requirements.filterIsInstance<DefeatRequirement>().forEach { defeatRequirement ->
if (defeatRequirement.target.matches(faintedPokemon.effectedPokemon)) {
val progress = pokemon.evolutionProxy.current().progressFirstOrCreate({ it is DefeatEvolutionProgress && it.currentProgress().target == defeatRequirement.target }) { DefeatEvolutionProgress() }
progress.updateProgress(DefeatEvolutionProgress.Progress(defeatRequirement.target, progress.currentProgress().amount + 1))
val progress = pokemon.evolutionProxy.current().progressFirstOrCreate({ it is DefeatTransformationProgress && it.currentProgress().target == defeatRequirement.target }) { DefeatTransformationProgress() }
progress.updateProgress(DefeatTransformationProgress.Progress(defeatRequirement.target, progress.currentProgress().amount + 1))
}
}
}
@@ -8,11 +8,11 @@
package com.cobblemon.mod.common.api.drop
import com.cobblemon.mod.common.api.pokemon.evolution.requirement.EvolutionRequirement
import com.cobblemon.mod.common.api.pokemon.transformation.requirement.TransformationRequirement
import com.cobblemon.mod.common.pokemon.Pokemon
class EvolutionItemDropEntry : ItemDropEntry() {
val requirements: MutableSet<EvolutionRequirement> = mutableSetOf()
val requirements: MutableSet<TransformationRequirement> = mutableSetOf()
override fun canDrop(pokemon: Pokemon?): Boolean = pokemon != null && requirements.all { it.check(pokemon) }
}
@@ -12,8 +12,8 @@ import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.moLangFunctionMap
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
/**
* Fired when an evolution is accepted.
@@ -10,8 +10,8 @@ package com.cobblemon.mod.common.api.events.pokemon.evolution
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
/**
* Fired after an evolution finishes.
@@ -8,9 +8,9 @@
package com.cobblemon.mod.common.api.events.pokemon.evolution
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.evolution.EvolutionDisplay
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.evolution.EvolutionDisplay
/**
* Fired before an evolution is sent to clients.
@@ -8,7 +8,7 @@
package com.cobblemon.mod.common.api.events.pokemon.evolution
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.cobblemon.mod.common.pokemon.Pokemon
/**
@@ -12,8 +12,8 @@ import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.moLangFunctionMap
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.cobblemon.mod.common.util.getBoolean
/**
@@ -14,7 +14,7 @@ import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.net.messages.client.data.FossilRegistrySyncPacket
import com.cobblemon.mod.common.pokemon.evolution.adapters.LegacyItemConditionWrapperAdapter
import com.cobblemon.mod.common.pokemon.transformation.adapters.LegacyItemConditionWrapperAdapter
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.ItemLikeConditionAdapter
import com.cobblemon.mod.common.util.adapters.pokemonPropertiesShortAdapter
@@ -74,7 +74,7 @@ abstract class RequestManager<T : ServerPlayerActionRequest> {
?: this.getInboundRequest(receiver.uuid, requestID)
/** Determines whether [player] can receive a [T]. */
open fun isBusy(player: ServerPlayer): Boolean = player.isInBattle() || player.isTrading() || player.party().find { it.entity?.isBusy == true } != null
open fun isBusy(player: ServerPlayer): Boolean = player.isBusy() || player.isPartyBusy()
/** Determines if the [target] is a valid interaction request target. */
abstract fun isValidInteraction(player: ServerPlayer, target: ServerPlayer): Boolean
@@ -29,7 +29,6 @@ import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.NPCProvider
import com.cobblemon.mod.common.api.pokedex.*
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.scripting.CobblemonScripts
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
@@ -57,9 +56,10 @@ import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormParticl
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.pokemon.evolution.variants.ItemInteractionEvolution
import com.cobblemon.mod.common.pokemon.evolution.variants.LevelUpEvolution
import com.cobblemon.mod.common.pokemon.evolution.variants.TradeEvolution
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.cobblemon.mod.common.pokemon.transformation.triggers.ItemInteractionTrigger
import com.cobblemon.mod.common.pokemon.transformation.triggers.LevelUpTrigger
import com.cobblemon.mod.common.pokemon.transformation.triggers.TradeTrigger
import com.cobblemon.mod.common.util.*
import com.mojang.datafixers.util.Either
import net.minecraft.commands.arguments.EntityAnchorArgument
@@ -1180,16 +1180,15 @@ object MoLangFunctions {
map.put("result") { evolution.result.asMoLangValue() }
if (evolution is LevelUpEvolution) {
if (evolution.trigger is LevelUpTrigger) {
map.put("is_level_up") { DoubleValue.ONE }
} else if (evolution is TradeEvolution) {
} else if (evolution.trigger is TradeTrigger) {
map.put("is_trade") { DoubleValue.ONE }
} else if (evolution is ItemInteractionEvolution) {
} else if (evolution.trigger is ItemInteractionTrigger) {
map.put("is_item") { DoubleValue.ONE }
}
map.put("is_optional") { DoubleValue(evolution.optional) }
map.put("consumes_held_item") { DoubleValue(evolution.consumeHeldItem) }
map
}
@@ -12,6 +12,7 @@ import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.data.ShowdownIdentifiable
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.moves.categories.DamageCategory
@@ -56,7 +57,7 @@ open class MoveTemplate(
val critRatio: Double,
val effectChances: Array<Double>,
val actionEffect: ActionEffectTimeline?
) {
) : ShowdownIdentifiable {
val struct: MoStruct by lazy {
QueryStruct(hashMapOf())
.addFunction("name") { StringValue(name) }
@@ -192,4 +193,6 @@ open class MoveTemplate(
}
return this.elementalType
}
override fun showdownId() = ShowdownIdentifiable.REGEX.replace(this.name.lowercase(), "")
}
@@ -466,29 +466,18 @@ open class PokemonProperties {
}
fun matches(properties: PokemonProperties): Boolean {
return properties.asString() == this.asString()
// Custom properties check could be duplicated but can't assume 3rd party hasn't done anything type specific
return this.commonMatches(properties) && this.customProperties.containsAll(properties.customProperties)
}
// Undefined properties will match whatever they're compared with
private fun commonMatches(pokemon: Pokemon): Boolean {
level?.takeIf { it != pokemon.level }?.let { return false }
shiny?.takeIf { it != pokemon.shiny }?.let { return false }
gender?.takeIf { it != pokemon.gender }?.let { return false }
species?.run {
try {
val species = if (this == "random") {
PokemonSpecies.species.random()
} else {
PokemonSpecies.getByIdentifier(this.asIdentifierDefaultingNamespace()) ?: return@run
}
if (pokemon.species != species) {
return false
}
} catch (e: ResourceLocationException) {
return false
}
}
species?.takeIf { it.asSpecies() != pokemon.species }?.let { return false }
nickname?.takeIf { it.string != pokemon.nickname?.string }?.let { return false }
form?.takeIf { !it.equals(pokemon.form.name, true) }?.let { return false }
form?.takeIf { !it.equals(pokemon.form.formOnlyShowdownId(), true) }?.let { return false }
friendship?.takeIf { it != pokemon.friendship }?.let { return false }
pokeball?.takeIf { it != pokemon.caughtBall.name.toString() }?.let { return false }
nature?.takeIf { it != pokemon.nature.name.toString() }?.let { return false }
@@ -527,6 +516,36 @@ open class PokemonProperties {
return true
}
// Undefined properties will match whatever they're compared with
private fun commonMatches(properties: PokemonProperties): Boolean {
level?.takeIf { it != properties.level }?.let { return false }
shiny?.takeIf { it != properties.shiny }?.let { return false }
gender?.takeIf { it != properties.gender }?.let { return false }
species?.takeIf { it.asSpecies() != properties.species?.asSpecies() }?.let { return false }
nickname?.takeIf { it.string != properties.nickname?.string }?.let { return false }
form?.takeIf { it != properties.form }?.let { return false }
friendship?.takeIf { it != properties.friendship }?.let { return false }
pokeball?.takeIf { it != properties.pokeball }?.let { return false }
nature?.takeIf { it != properties.nature }?.let { return false }
ability?.takeIf { it != properties.ability }?.let { return false }
status?.takeIf { it != properties.status }?.let { return false }
ivs?.forEach{ stat ->
if (stat.value != properties.ivs?.get(stat.key)) { return false }
}
evs?.forEach{ stat ->
if (stat.value != properties.evs?.get(stat.key)) { return false }
}
type?.takeIf { it != properties.type }?.let { return false }
teraType?.takeIf { it != properties.teraType }?.let { return false }
dmaxLevel?.takeIf { it != properties.dmaxLevel }?.let { return false }
gmaxFactor?.takeIf { it != properties.gmaxFactor }?.let { return false }
tradeable?.takeIf { it != properties.tradeable }?.let { return false }
originalTrainer?.takeIf { it != properties.originalTrainer }?.let{ return false }
originalTrainerType?.takeIf { it != properties.originalTrainerType }?.let{ return false }
moves?.takeIf { it.any { move -> properties.moves?.none { it == move } == true } }?.let { return false }
return true
}
// TODO Review the need for this, usage for us is in commands that deviate from the behavior established by multiple Pokémon query equivalent
fun isSubSetOf(properties: PokemonProperties): Boolean {
level?.takeIf { it != properties.level }?.let { return false }
@@ -798,4 +817,15 @@ open class PokemonProperties {
return potentialAbility.template.create(false, potentialAbility.priority)
}
private fun String.asSpecies() =
try {
if (this == "random") {
PokemonSpecies.species.random()
} else {
PokemonSpecies.getByIdentifier(this.asIdentifierDefaultingNamespace())
}
}
catch (e: ResourceLocationException) {
null
}
}
@@ -12,6 +12,8 @@ import com.bedrockk.molang.Expression
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.abilities.AbilityPool
import com.cobblemon.mod.common.api.abilities.AbilityTemplate
import com.cobblemon.mod.common.api.abilities.CommonAbility
import com.cobblemon.mod.common.api.abilities.PotentialAbility
import com.cobblemon.mod.common.api.ai.SleepDepth
import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
import com.cobblemon.mod.common.api.data.JsonDataRegistry
@@ -24,14 +26,15 @@ import com.cobblemon.mod.common.api.moves.adapters.MoveTemplateAdapter
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffect
import com.cobblemon.mod.common.api.pokemon.effect.adapter.ShoulderEffectAdapter
import com.cobblemon.mod.common.api.pokemon.egg.EggGroup
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.evolution.PreEvolution
import com.cobblemon.mod.common.api.pokemon.evolution.requirement.EvolutionRequirement
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.ExperienceGroup
import com.cobblemon.mod.common.api.pokemon.experience.ExperienceGroupAdapter
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
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.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.riding.behaviour.RidingBehaviourSettings
import com.cobblemon.mod.common.api.riding.stats.RidingStatDefinition
@@ -42,11 +45,16 @@ import com.cobblemon.mod.common.net.messages.client.data.SpeciesRegistrySyncPack
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.pokemon.SpeciesAdditions
import com.cobblemon.mod.common.pokemon.evolution.adapters.CobblemonEvolutionAdapter
import com.cobblemon.mod.common.pokemon.evolution.adapters.CobblemonPreEvolutionAdapter
import com.cobblemon.mod.common.pokemon.evolution.adapters.CobblemonRequirementAdapter
import com.cobblemon.mod.common.pokemon.evolution.adapters.LegacyItemConditionWrapperAdapter
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
import com.cobblemon.mod.common.pokemon.abilities.SpecialAbility
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
import com.cobblemon.mod.common.pokemon.transformation.adapters.*
import com.cobblemon.mod.common.pokemon.transformation.form.GigantamaxForm
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.pokemon.transformation.requirements.HeldItemRequirement
import com.cobblemon.mod.common.pokemon.transformation.requirements.MoveSetRequirement
import com.cobblemon.mod.common.util.adapters.*
import com.cobblemon.mod.common.util.adapters.RidingBehaviourSettingsAdapter
import com.cobblemon.mod.common.util.cobblemonResource
@@ -69,6 +77,7 @@ import net.minecraft.world.level.biome.Biome
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.levelgen.structure.Structure
import net.minecraft.world.phys.AABB
import java.util.NoSuchElementException
object PokemonSpecies : JsonDataRegistry<Species> {
@@ -84,10 +93,8 @@ object PokemonSpecies : JsonDataRegistry<Species> {
.registerTypeAdapter(ExperienceGroup::class.java, ExperienceGroupAdapter)
.registerTypeAdapter(EntityDimensions::class.java, EntityDimensionsAdapter)
.registerTypeAdapter(Learnset::class.java, LearnsetAdapter)
.registerTypeAdapter(Evolution::class.java, CobblemonEvolutionAdapter)
.registerTypeAdapter(AABB::class.java, BoxAdapter)
.registerTypeAdapter(AbilityPool::class.java, AbilityPoolAdapter)
.registerTypeAdapter(EvolutionRequirement::class.java, CobblemonRequirementAdapter)
.registerTypeAdapter(PreEvolution::class.java, CobblemonPreEvolutionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(Set::class.java, Evolution::class.java).type, LazySetAdapter(Evolution::class))
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
@@ -106,7 +113,10 @@ object PokemonSpecies : JsonDataRegistry<Species> {
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Structure::class.java).type, StructureLikeConditionAdapter)
.registerTypeAdapter(EggGroup::class.java, EggGroupAdapter)
.registerTypeAdapter(MobEffect::class.java, RegistryElementAdapter<MobEffect>(BuiltInRegistries::MOB_EFFECT))
.registerTypeAdapter(TransformationTrigger::class.java, CobblemonTriggerAdapter)
.registerTypeAdapter(TransformationRequirement::class.java, CobblemonRequirementAdapter)
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
.registerTypeAdapter(FormData::class.java, FormAdapter)
.registerTypeAdapter(RidingBehaviourSettings::class.java, RidingBehaviourSettingsAdapter)
.registerTypeAdapter(RidingStatDefinition::class.java, RidingStatDefinitionAdapter)
.registerTypeAdapter(Expression::class.java, ExpressionAdapter)
@@ -126,6 +136,9 @@ object PokemonSpecies : JsonDataRegistry<Species> {
get() = this.speciesByIdentifier.values
val implemented = mutableListOf<Species>()
val forms: Collection<FormData>
get() = this.species.flatMap { it.allForms }
init {
SpeciesAdditions.observable.subscribe {
this.species.forEach(Species::initialize)
@@ -225,34 +238,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"
@@ -260,35 +257,132 @@ object PokemonSpecies : JsonDataRegistry<Species> {
}
val genderRatio = if (this.gender == null)
mapOf(
"maleRatio" to (form?.maleRatio ?: species.maleRatio),
"femaleRation" to (1F - (form?.maleRatio ?: species.maleRatio))
"M" to (form.maleRatio),
"F" 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
val abilities = form.showdownAbilityMapping()
// Common fields exempt from standard forms
val baseSpecies = if (form !is StandardForm) form.species.standardForm.showdownName() else null
val forme = if (form !is StandardForm) form.name else null
/**
* The following fields are optional and currently omitted
*
* cosmeticFormes (not needed, we have species features)
* addedType (tertiary type [by name or showdownid] added by moves or abilities)
* tier (smogon tryhard bs)
* doublesTier (smogon tryhard bs)
* natDexTier (smogon tryhard bs)
* evoType (the evolution method used to evolve from 'prevo'; trade', 'useItem', 'levelMove', 'levelExtra', 'levelFriendship', 'levelHold', 'other')
* evoMove (the move [by name or showdownid] used to evolve from 'prevo')
* evoLevel (the minimum level required to evolve from 'prevo')
* canHatch (flag for baby pokemon)
* color (color, value is arbitrary)
* tags (flags for filtering pokemon; 'Mega', 'Mythical', 'Sub-Legendary', 'Restricted Legendary', 'Paradox')
* unreleasedHidden (whether this pokemon has an unreleased hidden ability)
* maleOnlyHidden (whether it is only possible to get the hidden ability on a male pokemon)
* gmaxUnreleased (whether this pokemon has an unreleased gmax form)
* pokemonGoData (no one cares)
*/
}
private fun createShowdownName(species: Species): String {
if (species.resourceIdentifier.namespace == Cobblemon.MODID) {
return species.name
/**
* The representation of a [PermanentForm] in the context of Showdown.
*
* @param form The [PermanentForm] being converted into a species.
*/
internal open class ShowdownPermanentForm(form: PermanentForm) : ShowdownSpecies(form) {
// species this form evolves from
val prevo = 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 = form.evolutions.map { "" }
// not fully evolved
val nfe = this.evos.isNotEmpty()
// whether there is a gmax form to change to
val canGigantamax: String? = form.changeableForms.filterIsInstance<GigantamaxForm>().firstOrNull()?.name // TODO id better
}
/**
* 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) {
// list of all forms descending from this base species
val otherFormes = form.flattenForms().map { it.showdownName() }
// list of forms [by name or showdownid] in the order they appear in game [union of baseSpecies, otherFormes, and cosmeticFormes] with the exception of ending with 'Gmax' forms
val formeOrder = if (this.otherFormes.isNotEmpty()) arrayListOf(this.name, *this.otherFormes.toTypedArray()) else null
// TODO we don't enforce any form order with our Species registration - right now this looks like only a problem with Mega Rayquaza, but investigate changes needed on Showdown's end to enable these lists to be more dynamic
}
/**
* The representation of a [TemporaryForm] in the context of Showdown.
*
* @param form The [TemporaryForm] being converted into a species.
*/
internal class ShowdownTemporaryForm(form: TemporaryForm) : ShowdownSpecies(form) {
// form change can occur during a battle
val battleOnly = if (form.battleOnly) form.parentForm.showdownName() else null
// form change can occur outside of battle (mutually exclusive with battleOnly)
val changesFrom = if (!form.battleOnly) form.parentForm.showdownName() else null
// pokemon must know this move to be in this form
val requiredMove = form.requirements.filterIsInstance<MoveSetRequirement>().firstOrNull()?.move?.showdownId() // TODO update showdown to support 'requiredMoves'
// pokemon must hold one of these item(s) to be in this form
val requiredItems = form.requirements.filterIsInstance<HeldItemRequirement>().firstOrNull()?.let { requirement ->
try {
requirement.itemCondition.items.get().map { BuiltInRegistries.ITEM.getKey(it.value()).path } // TODO held items on showdown are ignorant of namespace
}
catch (e: NoSuchElementException)
{
Cobblemon.LOGGER.error("Failed to resolve required held item for ${form.showdownId()}}") // TODO this needs to be done after tags are loaded...
null
}
}
return "${species.resourceIdentifier.namespace}:${species.name}"
}
/** Accommodate for Showdown bullshit. */
/*
private fun TemporaryForm.showdownParentForm(): Any {
if (this.parentOverrides.isNotEmpty())
return this.species.standardForm.flattenForms().filter {
parentOverrides.contains(it.showdownId())
}.map {
it.showdownName()
}
else
return parentForm.showdownName()
}
*/
/**
* Constructs a mapping of the [PotentialAbility]s belonging to a [FormData] in Showdown identifiable format.
*
* @return A map of type (numeric for [CommonAbility]s, H for [HiddenAbility], S for [SpecialAbility]) to showdownid.
*/
private fun FormData.showdownAbilityMapping(): Map<String, String>? {
// potential abilities
val commonAbilities = this.abilities.filterIsInstance<CommonAbility>()
val hiddenAbility = this.abilities.find { it is HiddenAbility }
val specialAbility = this.abilities.find { it is SpecialAbility }
// can have multiple common abilities
val abilities = mutableMapOf<String, String>()
val mapping: (String, PotentialAbility) -> Unit = { key, ability ->
abilities.put(key, ability.template.showdownId())
}
// can only have one hidden and one special ability
commonAbilities.forEachIndexed { index, ability -> mapping(index.toString(), ability) }
hiddenAbility?.let { ability -> mapping("H", ability) }
specialAbility?.let { ability -> mapping("S", ability) }
return if (abilities.isEmpty()) null else abilities
}
}
@@ -0,0 +1,24 @@
package com.cobblemon.mod.common.api.pokemon.aspect
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeature
import com.cobblemon.mod.common.pokemon.Pokemon
/**
* A provider of aspects for a [SpeciesFeature].
*
* @author Segfault Guy
* @since Feb 12th, 2024
*/
interface FeatureAspectProvider: AspectProvider {
var default: String?
var isAspect: Boolean
/** Returns true if this [aspect] is provided by this provider. */
fun matches(aspect: String): Boolean
/** Creates a [SpeciesFeature] that provides the respective [aspect]. */
fun from(aspect: String): SpeciesFeature<*>?
/** Sets the [aspect] that's provided. */
fun set(pokemon: Pokemon, aspect: String)
}
@@ -24,11 +24,11 @@ interface SingleConditionalAspectProvider : AspectProvider {
fun getForFeature(name: String): SingleConditionalAspectProvider {
return object : SingleConditionalAspectProvider {
override val aspect: String = name
override fun meetsCondition(pokemon: Pokemon) = pokemon.getFeature<FlagSpeciesFeature>(name)?.enabled == true
override fun meetsCondition(pokemon: Pokemon) = pokemon.getFeature<FlagSpeciesFeature>(name)?.value == true
override fun meetsCondition(pokemonProperties: PokemonProperties) = pokemonProperties
.customProperties
.filterIsInstance<FlagSpeciesFeature>()
.any { it.name == name && it.enabled }
.any { it.name == name && it.value }
}
}
@@ -1,54 +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.api.pokemon.evolution
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.evolution.variants.ItemInteractionEvolution
import com.cobblemon.mod.common.pokemon.evolution.variants.TradeEvolution
/**
* Represents an evolution of a [Pokemon] that can only occur during specific actions and with added context.
* For the default implementations see [ItemInteractionEvolution] & [TradeEvolution].
*
* @param RC The context given at runtime when querying the [Evolution].
* @param TC The context that is serialized from JSON during species loading, this is what the [RC] is expected to match against.
* @author Licious
* @since March 19th, 2022
*/
interface ContextEvolution<RC, TC> : Evolution {
/**
* The target context for this [Evolution] to even be tested.
*/
val requiredContext: TC
/**
* Attempts to evolve the given [Pokemon] under the given context of type [RC].
*
* @param pokemon The [Pokemon] attempting to evolve.
* @param context The context of this query.
* @return If the evolution was successful.
*/
fun attemptEvolution(pokemon: Pokemon, context: RC): Boolean {
if (this.testContext(pokemon, context) && super.test(pokemon)) {
return super.evolve(pokemon)
}
return false
}
/**
* Checks if the given context is valid for the [requiredContext].
*
* @param pokemon The [Pokemon] attempting to evolve.
* @param context The context of this query.
* @return If the context matched the [requiredContext].
*/
fun testContext(pokemon: Pokemon, context: RC): Boolean
}
@@ -1,69 +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.api.pokemon.evolution
import com.cobblemon.mod.common.api.pokemon.evolution.progress.EvolutionProgress
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.evolution.controller.ClientEvolutionController
import com.cobblemon.mod.common.pokemon.evolution.controller.ServerEvolutionController
/**
* Responsible for holding all available [EvolutionLike]s in the [Pokemon].
* Also handles all the networking behind them.
* For the Cobblemon default implementations see [ClientEvolutionController] & [ServerEvolutionController].
*
* @author Licious
* @since April 28th, 2022
*/
interface EvolutionController<T : EvolutionLike, out I : PreProcessor> : MutableSet<T> {
/**
* Resolves the [Pokemon] attached to this controller.
*
* @return The [Pokemon] this controller is attached to.
*/
fun pokemon(): Pokemon
/**
* Starts the given evolution on this controller.
* All the necessary networking will be handled.
*
* @param evolution The [EvolutionLike] starting.
*/
fun start(evolution: T)
/**
* Returns an immutable collection of [EvolutionProgress]es attached to this controller.
*
* @return The currently tracked [EvolutionProgress]es.
*/
fun progress(): Collection<EvolutionProgress<*>>
/**
* Attaches a [EvolutionProgress] to this controller.
*
* @param P The type of [EvolutionProgress].
* @param progress The [EvolutionProgress] being attached for tracking.
* @return The instance being tracked.
*/
fun <P : EvolutionProgress<*>> trackProgress(progress: P): P
/**
* Attempts to find an [EvolutionProgress] in [progress] or creates and tracks using [trackProgress].
*
* @param P The type of [EvolutionProgress].
* @param predicate The lambda attempting to find specific progress. This needs to ensure the [EvolutionProgress] is of type [P] or the cast will fail.
* @param progressFactory The lambda creating a new instance of type [P] if [predicate] can't find an element.
* @return The found match in [progress] or the created and now tracked with [trackProgress].
*/
fun <P : EvolutionProgress<*>> progressFirstOrCreate(predicate: (progress: EvolutionProgress<*>) -> Boolean, progressFactory: () -> P): P
fun asIntermediate(): I
}
@@ -1,48 +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.api.pokemon.evolution
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.pokemon.evolution.CobblemonEvolutionDisplay
import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.RecordCodecBuilder
/**
* Represents an evolution of a [Pokemon], this is the client side counterpart of [Evolution].
* This has no attachments to any data regarding the evolution itself and only serves for display purposes and basic communication.
*
* @author Licious
* @since April 28th, 2022
*/
interface EvolutionDisplay : EvolutionLike {
/**
* The [Species] of the evolution result.
*/
val species: Species
/**
* The aspects of the evolution result.
* These are used by the client to resolve the entity it needs to display.
*/
val aspects: Set<String>
companion object {
@JvmStatic
val CODEC: Codec<EvolutionDisplay> = RecordCodecBuilder.create { instance ->
instance.group(
Codec.STRING.fieldOf("id").forGetter(EvolutionDisplay::id),
Species.BY_IDENTIFIER_CODEC.fieldOf("species").forGetter(EvolutionDisplay::species),
Codec.list(Codec.STRING).fieldOf("aspects").forGetter { it.aspects.toList() }
).apply(instance) { id, species, aspects -> CobblemonEvolutionDisplay(id, species, aspects.toSet()) }
}
}
}
@@ -1,59 +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.api.pokemon.evolution
/**
* Responsible for holding the different [EvolutionController] implementations based on the logical side.
* If you haven't already checked your side previously you can use [isClient].
*
* @param C The type of [EvolutionLike] on the client side.
* @param S The type of [EvolutionLike] on the server side.
* @param CI The type of [PreProcessor] on the client side.
* @param SI The type of [PreProcessor] on the server side.
*
* @author Licious
* @since June 18th, 2022
*/
interface EvolutionProxy<C : EvolutionLike, S : EvolutionLike, CI : PreProcessor, SI : PreProcessor> {
/**
* Checks if the current logical side is the client.
*
* @return True if the current logical side is the client.
*/
fun isClient(): Boolean
/**
* Returns the current [EvolutionController].
* Keep in mind this will have type erasure and as such most operations will not be possible.
* It's recommended to work with [client] or [server] when possible.
*
* @return The current [EvolutionController].
*/
fun current(): EvolutionController<out EvolutionLike, *>
/**
* The client side implementation of the [EvolutionController].
*
* @throws [ClassCastException] if called from the server side.
*
* @return The client side implementation of the [EvolutionController] if possible.
*/
fun client(): EvolutionController<C, CI>
/**
* The server side implementation of the [EvolutionController].
*
* @throws [ClassCastException] if called from the client side.
*
* @return The server side implementation of the [EvolutionController] if possible.
*/
fun server(): EvolutionController<S, SI>
}
@@ -1,43 +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.api.pokemon.evolution
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.evolution.variants.LevelUpEvolution
/**
* Represents an evolution of a [Pokemon] that can occur without any additional context or actions.
* For the default implementation see [LevelUpEvolution].
*
* @author Licious
* @since March 19th, 2022
*/
interface PassiveEvolution : Evolution {
/**
* Checks if the given [Pokemon] satisfies the requirements.
* If yes the evolution will attempt to start.
*
* @param pokemon The [Pokemon] being tested.
* @return If the [Pokemon] will evolve.
*/
fun attemptEvolution(pokemon: Pokemon): Boolean {
if (super.test(pokemon)) {
return super.evolve(pokemon)
}
return false
}
/**
* If the evolution will disappear once the conditions are no longer met.
*/
val permanent: Boolean
}
@@ -1,35 +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.api.pokemon.evolution.adapters
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.pokemon.evolution.adapters.CobblemonEvolutionAdapter
import com.google.gson.JsonDeserializer
import com.google.gson.JsonSerializer
import kotlin.reflect.KClass
/**
* Saves and loads [Evolution]s with JSON.
* For the default implementation see [CobblemonEvolutionAdapter].
*
* @author Licious
* @since March 20th, 2022
*/
interface EvolutionAdapter : JsonDeserializer<Evolution>, JsonSerializer<Evolution> {
/**
* Registers the given type of [Evolution] to it's associated ID for deserialization.
*
* @param T The type of [Evolution].
* @param id The id of the evolution event.
* @param type The [KClass] of the type.
*/
fun <T : Evolution> registerType(id: String, type: KClass<T>)
}
@@ -1,35 +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.api.pokemon.evolution.adapters
import com.cobblemon.mod.common.api.pokemon.evolution.requirement.EvolutionRequirement
import com.cobblemon.mod.common.pokemon.evolution.adapters.CobblemonRequirementAdapter
import com.google.gson.JsonDeserializer
import com.google.gson.JsonSerializer
import kotlin.reflect.KClass
/**
* Saves and loads [EvolutionRequirement]s with JSON.
* For the default implementation see [CobblemonRequirementAdapter].
*
* @author Licious
* @since March 21st, 2022
*/
interface RequirementAdapter : JsonDeserializer<EvolutionRequirement>, JsonSerializer<EvolutionRequirement> {
/**
* Registers the given type of [EvolutionRequirement] to it's associated ID for deserialization.
*
* @param T The type of [EvolutionRequirement].
* @param id The id of the evolution event.
* @param type The [KClass] of the type.
*/
fun <T : EvolutionRequirement> registerType(id: String, type: KClass<T>)
}
@@ -1,66 +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.api.pokemon.evolution.progress
import com.cobblemon.mod.common.pokemon.evolution.controller.ServerEvolutionController
import com.cobblemon.mod.common.pokemon.evolution.progress.*
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.serialization.Codec
import com.mojang.serialization.Lifecycle
import net.minecraft.core.MappedRegistry
import net.minecraft.core.Registry
import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation
import kotlin.jvm.Throws
object EvolutionProgressTypes {
private val REGISTRY: Registry<EvolutionProgressType<*>> = MappedRegistry(
ResourceKey.createRegistryKey(cobblemonResource("evolution_progress")), Lifecycle.stable()
)
@JvmStatic
val DAMAGE_TAKEN = this.registerType(DamageTakenEvolutionProgress.ID, EvolutionProgressType(DamageTakenEvolutionProgress.CODEC))
@JvmStatic
val DEFEAT = this.registerType(DefeatEvolutionProgress.ID, EvolutionProgressType(DefeatEvolutionProgress.CODEC))
@JvmStatic
val LAST_BATTLE_CRITICAL_HITS = this.registerType(LastBattleCriticalHitsEvolutionProgress.ID, EvolutionProgressType(LastBattleCriticalHitsEvolutionProgress.CODEC))
@JvmStatic
val RECOIL = this.registerType(RecoilEvolutionProgress.ID, EvolutionProgressType(RecoilEvolutionProgress.CODEC))
@JvmStatic
val USE_MOVE = this.registerType(UseMoveEvolutionProgress.ID, EvolutionProgressType(UseMoveEvolutionProgress.CODEC))
/**
* Registers an evolution progress.
*
* @param T The type of the [EvolutionProgress].
* @param id The [Identifier].
* @param type The [EvolutionProgressType] record.
* @return The registered [EvolutionProgressType] of type [T].
*
* @throws [IllegalStateException] if [Registry.register] fails.
*/
@Throws(IllegalStateException::class)
@JvmStatic
fun <T : EvolutionProgress<*>> registerType(id: ResourceLocation, type: EvolutionProgressType<T>): EvolutionProgressType<T> {
return Registry.register(REGISTRY, id, type)
}
/**
* Creates a codec instance of this registry.
*
* @return The generated [Codec] of [EvolutionProgress].
*/
fun codec(): Codec<EvolutionProgress<*>> = REGISTRY.byNameCodec().dispatch(
ServerEvolutionController.ID_KEY,
{ it.type() },
{ it.codec }
)
}
@@ -12,7 +12,7 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.OriginalTrainerType
import com.cobblemon.mod.common.pokemon.evolution.requirements.LevelRequirement
import com.cobblemon.mod.common.pokemon.transformation.requirements.LevelRequirement
import kotlin.math.pow
import kotlin.math.roundToInt
@@ -9,7 +9,7 @@
package com.cobblemon.mod.common.api.pokemon.feature
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.aspect.AspectProvider
import com.cobblemon.mod.common.api.pokemon.aspect.FeatureAspectProvider
import com.cobblemon.mod.common.api.properties.CustomPokemonPropertyType
import com.cobblemon.mod.common.client.gui.summary.featurerenderers.SummarySpeciesFeatureRenderer
import com.cobblemon.mod.common.pokemon.Pokemon
@@ -29,11 +29,11 @@ import net.minecraft.network.RegistryFriendlyByteBuf
*/
open class ChoiceSpeciesFeatureProvider(
override var keys: List<String>,
var default: String? = null,
override var default: String? = null,
var choices: List<String> = listOf(),
var isAspect: Boolean = true,
override var isAspect: Boolean = true,
var aspectFormat: String = "{{choice}}"
) : SynchronizedSpeciesFeatureProvider<StringSpeciesFeature>, CustomPokemonPropertyType<StringSpeciesFeature>, AspectProvider {
) : SynchronizedSpeciesFeatureProvider<StringSpeciesFeature>, CustomPokemonPropertyType<StringSpeciesFeature>, FeatureAspectProvider {
override var needsKey = true
override var visible = false
fun getAspect(feature: StringSpeciesFeature) = aspectFormat.substitute("choice", feature.value)
@@ -76,6 +76,28 @@ open class ChoiceSpeciesFeatureProvider(
return aspects
}
fun parseValue(aspect: String): String? {
if (!matches(aspect)) return null
val head = aspectFormat.substringBefore("{{choice}}")
val tail = aspectFormat.substringAfter("{{choice}}").takeIf { it.isNotBlank() }
var value = aspect.substringAfter(head)
tail?.let { value = value.substringBefore(it) }
return value
}
override fun matches(aspect: String): Boolean = this.isAspect && this.getAllAspects().contains(aspect)
override fun from(aspect: String): SpeciesFeature<*>? {
val value = parseValue(aspect)
return if (value != null && choices.contains(value)) this.fromString(value) else null
}
override fun set(pokemon: Pokemon, aspect: String) {
val value = parseValue(aspect)
if (value != null && choices.contains(value)) this.get(pokemon)?.value = value
}
override fun examples() = choices
internal constructor(): this(emptyList())
@@ -123,7 +145,9 @@ open class ChoiceSpeciesFeatureProvider(
override fun provide(pokemon: Pokemon): Set<String> {
return if (isAspect) {
get(pokemon)?.let { setOf(getAspect(it)) } ?: emptySet()
get(pokemon)?.let {
setOf(getAspect(it))
} ?: emptySet()
} else {
emptySet()
}
@@ -9,7 +9,7 @@
package com.cobblemon.mod.common.api.pokemon.feature
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.aspect.AspectProvider
import com.cobblemon.mod.common.api.pokemon.aspect.FeatureAspectProvider
import com.cobblemon.mod.common.api.properties.CustomPokemonProperty
import com.cobblemon.mod.common.api.properties.CustomPokemonPropertyType
import com.cobblemon.mod.common.client.gui.summary.featurerenderers.SummarySpeciesFeatureRenderer
@@ -30,65 +30,74 @@ import kotlin.random.Random
* @author Hiroku
* @since May 13th, 2022
*/
open class FlagSpeciesFeature(override val name: String) : SynchronizedSpeciesFeature, CustomPokemonProperty {
open class FlagSpeciesFeature(override val name: String) : SynchronizedSpeciesFeature<Boolean>, CustomPokemonProperty {
constructor(name: String, enabled: Boolean): this(name) {
this.enabled = enabled
this.value = enabled
}
var enabled = false
override var value: Boolean = false
override fun saveToNBT(pokemonNBT: CompoundTag): CompoundTag {
pokemonNBT.putBoolean(name, enabled)
pokemonNBT.putBoolean(name, value)
return pokemonNBT
}
override fun loadFromNBT(pokemonNBT: CompoundTag): SpeciesFeature {
enabled = if (pokemonNBT.contains(name)) pokemonNBT.getBoolean(name) else enabled
override fun loadFromNBT(pokemonNBT: CompoundTag): SpeciesFeature<Boolean> {
value = if (pokemonNBT.contains(name)) pokemonNBT.getBoolean(name) else value
return this
}
override fun saveToJSON(pokemonJSON: JsonObject): JsonObject {
pokemonJSON.addProperty(name, enabled)
pokemonJSON.addProperty(name, value)
return pokemonJSON
}
override fun loadFromJSON(pokemonJSON: JsonObject): SpeciesFeature {
override fun loadFromJSON(pokemonJSON: JsonObject): SpeciesFeature<Boolean> {
val isEnabled = pokemonJSON.get(name)?.asBoolean
enabled = isEnabled ?: this.enabled
value = isEnabled ?: this.value
return this
}
override fun saveToBuffer(buffer: RegistryFriendlyByteBuf, toClient: Boolean) {
buffer.writeBoolean(enabled)
buffer.writeBoolean(value)
}
override fun loadFromBuffer(buffer: RegistryFriendlyByteBuf) {
enabled = buffer.readBoolean()
value = buffer.readBoolean()
}
override fun asString() = "$name=$enabled"
override fun asString() = "$name=$value"
override fun apply(pokemon: Pokemon) {
val featureProvider = SpeciesFeatures.getFeature(name) ?: return
if (featureProvider in SpeciesFeatures.getFeaturesFor(pokemon.species)) {
val existingFeature = pokemon.getFeature<FlagSpeciesFeature>(name)
if (existingFeature != null) {
existingFeature.enabled = enabled
existingFeature.value = value
} else {
pokemon.features.add(FlagSpeciesFeature(name, enabled))
pokemon.features.add(FlagSpeciesFeature(name, value))
}
pokemon.updateAspects()
}
}
override fun matches(pokemon: Pokemon) = pokemon.getFeature<FlagSpeciesFeature>(name)?.enabled == enabled
override fun matches(pokemon: Pokemon) = pokemon.getFeature<FlagSpeciesFeature>(name)?.value == value
}
class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpeciesFeature>, CustomPokemonPropertyType<FlagSpeciesFeature>, AspectProvider {
class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpeciesFeature>, CustomPokemonPropertyType<FlagSpeciesFeature>, FeatureAspectProvider {
override var keys: List<String> = emptyList()
// Uses get() = true because that way there's no backing field. It MUST be true, this way no JSON trickery will overwrite it
override val needsKey get() = true
var default: String? = null
var isAspect = true
override var default: String? = null
override var isAspect = true
override fun matches(aspect: String): Boolean = this.isAspect && this.keys.first() == aspect
override fun from(aspect: String): SpeciesFeature<*>? = if (matches(aspect)) fromString(null) else null
override fun set(pokemon: Pokemon, aspect: String) {
this.get(pokemon)?.value = true
}
override var visible: Boolean = false
override fun invoke(buffer: RegistryFriendlyByteBuf, name: String): FlagSpeciesFeature? {
@@ -170,7 +179,7 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
}
override fun provide(pokemon: Pokemon): Set<String> {
return if (isAspect && pokemon.getFeature<FlagSpeciesFeature>(keys.first())?.enabled == true) {
return if (isAspect && pokemon.getFeature<FlagSpeciesFeature>(keys.first())?.value == true) {
setOf(keys.first())
} else {
emptySet()
@@ -178,7 +187,7 @@ class FlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<FlagSpecie
}
override fun provide(properties: PokemonProperties): Set<String> {
return if (isAspect && properties.customProperties.filterIsInstance<FlagSpeciesFeature>().find { it.name == keys.first() }?.enabled == true) {
return if (isAspect && properties.customProperties.filterIsInstance<FlagSpeciesFeature>().find { it.name == keys.first() }?.value == true) {
setOf(keys.first())
} else {
emptySet()
@@ -84,7 +84,7 @@ object GlobalSpeciesFeatures : JsonDataRegistry<SpeciesFeatureProvider<*>> {
}
fun register(name: String, provider: SpeciesFeatureProvider<*>) = register(name, provider, isCoded = true)
fun <T : SpeciesFeature> register(name: String, providerLambda: () -> T) {
fun <T : SpeciesFeature<*>> register(name: String, providerLambda: () -> T) {
register(name, object : SpeciesFeatureProvider<T> {
override fun invoke(pokemon: Pokemon) = providerLambda()
override fun invoke(nbt: CompoundTag) = providerLambda().apply { loadFromNBT(nbt) }
@@ -26,8 +26,8 @@ import net.minecraft.world.phys.Vec3
* A species feature value that's just an integer. Complex stuff.
* @author Hiroku
*/
class IntSpeciesFeature(override var name: String) : SynchronizedSpeciesFeature, CustomPokemonProperty {
var value = 0
class IntSpeciesFeature(override var name: String) : SynchronizedSpeciesFeature<Int>, CustomPokemonProperty {
override var value = 0
constructor() : this("")
constructor(name: String, value: Int) : this(name) {
@@ -39,7 +39,7 @@ class IntSpeciesFeature(override var name: String) : SynchronizedSpeciesFeature,
return pokemonNBT
}
override fun loadFromNBT(pokemonNBT: CompoundTag): SynchronizedSpeciesFeature {
override fun loadFromNBT(pokemonNBT: CompoundTag): SynchronizedSpeciesFeature<Int> {
value = pokemonNBT.getInt(name)
return this
}
@@ -49,7 +49,7 @@ class IntSpeciesFeature(override var name: String) : SynchronizedSpeciesFeature,
return pokemonJSON
}
override fun loadFromJSON(pokemonJSON: JsonObject): SpeciesFeature {
override fun loadFromJSON(pokemonJSON: JsonObject): SpeciesFeature<Int> {
value = pokemonJSON.get(name).asInt
return this
}
@@ -24,10 +24,11 @@ import net.minecraft.nbt.CompoundTag
* @author Hiroku
* @since May 13th, 2022
*/
interface SpeciesFeature {
interface SpeciesFeature<T> {
val name: String
var value: T
fun saveToNBT(pokemonNBT: CompoundTag): CompoundTag
fun loadFromNBT(pokemonNBT: CompoundTag): SpeciesFeature
fun loadFromNBT(pokemonNBT: CompoundTag): SpeciesFeature<T>
fun saveToJSON(pokemonJSON: JsonObject): JsonObject
fun loadFromJSON(pokemonJSON: JsonObject): SpeciesFeature
fun loadFromJSON(pokemonJSON: JsonObject): SpeciesFeature<T>
}
@@ -21,7 +21,7 @@ import net.minecraft.nbt.CompoundTag
* @author Hiroku
* @since November 29th, 2022
*/
interface SpeciesFeatureProvider<T : SpeciesFeature> {
interface SpeciesFeatureProvider<T : SpeciesFeature<*>> {
/**
* Attempts to create a species feature for the given [Pokemon]. It is appropriate to check for an existing feature
* that is compatible with this type and transfer / retain that feature instead of constructing a new one. For example
@@ -24,14 +24,14 @@ import net.minecraft.network.RegistryFriendlyByteBuf
*/
class StringSpeciesFeature(
override val name: String,
var value: String
) : SynchronizedSpeciesFeature, CustomPokemonProperty {
override var value: String
) : SynchronizedSpeciesFeature<String>, CustomPokemonProperty {
override fun saveToNBT(pokemonNBT: CompoundTag): CompoundTag {
pokemonNBT.putString(name, value)
return pokemonNBT
}
override fun loadFromNBT(pokemonNBT: CompoundTag): SpeciesFeature {
override fun loadFromNBT(pokemonNBT: CompoundTag): SpeciesFeature<String> {
value = pokemonNBT.getString(name)?.takeIf { it.isNotBlank() }?.lowercase() ?: return this
return this
}
@@ -41,7 +41,7 @@ class StringSpeciesFeature(
return pokemonJSON
}
override fun loadFromJSON(pokemonJSON: JsonObject): SpeciesFeature {
override fun loadFromJSON(pokemonJSON: JsonObject): SpeciesFeature<String> {
value = pokemonJSON.get(name)?.asString?.lowercase() ?: return this
return this
}
@@ -16,4 +16,4 @@ import com.cobblemon.mod.common.api.serialization.BufferSerializer
* @author Hiroku
* @since November 13th, 2023
*/
interface SynchronizedSpeciesFeature : SpeciesFeature, BufferSerializer
interface SynchronizedSpeciesFeature<T> : SpeciesFeature<T>, BufferSerializer
@@ -22,7 +22,7 @@ import net.minecraft.network.RegistryFriendlyByteBuf
* @author Hiroku
* @since November 13th, 2023
*/
interface SynchronizedSpeciesFeatureProvider<T : SynchronizedSpeciesFeature> : SpeciesFeatureProvider<T>, BufferSerializer {
interface SynchronizedSpeciesFeatureProvider<T : SynchronizedSpeciesFeature<*>> : SpeciesFeatureProvider<T>, BufferSerializer {
var visible: Boolean
operator fun invoke(buffer: RegistryFriendlyByteBuf, name: String): T?
/** Gets the feature from this Pokémon, if it has been created yet. */
@@ -0,0 +1,88 @@
/*
* 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.api.pokemon.transformation
import com.cobblemon.mod.common.api.pokemon.transformation.controller.TransformationController
import com.cobblemon.mod.common.api.pokemon.transformation.requirement.TransformationRequirement
import com.cobblemon.mod.common.api.pokemon.transformation.trigger.ContextTrigger
import com.cobblemon.mod.common.api.pokemon.transformation.trigger.TransformationTrigger
import com.cobblemon.mod.common.pokemon.Pokemon
/**
* Represents a transformation of a [Pokemon]. This is the server side counterpart of [TransformationDisplay] and serves as
* the driver of the transformation. These are triggered by a player-driven or world-driven event.
*
* @author Segfault Guy
* @since Sept 8th, 2023
*/
interface Transformation : TransformationLike {
/** The [TransformationTrigger] to attempt this transformation. */
val trigger: TransformationTrigger
/** The [TransformationRequirement]s that need to be satisfied for this transformation. */
val requirements: Set<TransformationRequirement>
/** If this transformation can be deferred by the user and started whenever. */
val optional: Boolean
/** If this transformation needs its requirements reevaluated to remain valid in a [TransformationController]. */
val reevaluate: Boolean
/** Checks if all [TransformationRequirement]s are met. */
fun checkRequirements(pokemon: Pokemon) = this.requirements.all { requirement -> requirement.check(pokemon) }
/**
* Tests if the given [Pokemon] passes all the conditions and is ready to transform.
*
* @param pokemon The [Pokemon] being queried.
* @param context The optional [ContextTrigger.Context] needed to transform.
* @return Whether the [Transformation] can start.
*/
fun test(pokemon: Pokemon, context: ContextTrigger.Context?) = this.trigger.testTrigger(context) && this.checkRequirements(pokemon)
/**
* Starts this transformation if conditions are met.
*
* @param pokemon The [Pokemon] being transformed.
* @param context The optional [ContextTrigger.Context] needed to transform.
* @return Whether the [Transformation] was successful.
*/
fun start(pokemon: Pokemon, context: ContextTrigger.Context?): Boolean {
if (test(pokemon, context)) {
forceStart(pokemon)
return true
}
return false
}
/**
* Starts this transformation without evaluating the requirements.
*
* @param pokemon The [Pokemon] being transformed.
*/
fun forceStart(pokemon: Pokemon) { this.requirements.forEach { it.fulfill(pokemon) } }
/**
* Converts this transformation to its client side representation.
*
* @param pokemon The [Pokemon] being transformed.
* @return The [TransformationDisplay] representing this transformation on the client.
*/
fun convertToDisplay(pokemon: Pokemon): TransformationDisplay
/**
* Validates whether a transformation still meets the [TransformationRequirement]s.
* If [Transformation.reevaluate] is false, this check will be bypassed.
*
* @param pokemon the [Pokemon] to test against.
* @return Whether this transformation is still valid and can be made for [pokemon].
*/
fun reevaluate(pokemon: Pokemon) = !this.reevaluate || this.checkRequirements(pokemon)
}
@@ -0,0 +1,33 @@
/*
* 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.api.pokemon.transformation
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.Species
/**
* Represents a transformation of a [Pokemon], this is the client side counterpart of [Transformation].
* This has no attachments to any data regarding the transformation itself and only serves for display purposes and basic communication.
*
* @author Licious
* @since April 28th, 2022
*/
interface TransformationDisplay : TransformationLike {
/**
* The [Species] of the transformation result.
*/
val species: Species
/**
* The aspects of the transformation result.
* These are used by the client to resolve the entity it needs to display.
*/
val aspects: Set<String>
}
@@ -6,20 +6,20 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.pokemon.evolution
package com.cobblemon.mod.common.api.pokemon.transformation
/**
* Represents the base of an evolution.
* It can either be the full data container present on the server side [Evolution].
* Or the client side representation for display [EvolutionDisplay].
* Represents the base of a transformation.
* It can either be the full data container present on the server side [Transformation].
* Or the client side representation for display [TransformationDisplay].
*
* @author Licious
* @since April 28th, 2022
*/
interface EvolutionLike {
interface TransformationLike {
/**
* The unique id of the evolution.
* The unique id of the transformation.
* It should be human-readable, I.E pikachu_level
*/
val id: String
@@ -0,0 +1,35 @@
/*
* 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.api.pokemon.transformation.adapters
import com.cobblemon.mod.common.api.pokemon.transformation.requirement.TransformationRequirement
import com.cobblemon.mod.common.pokemon.transformation.adapters.CobblemonRequirementAdapter
import com.google.gson.JsonDeserializer
import com.google.gson.JsonSerializer
import kotlin.reflect.KClass
/**
* Saves and loads [TransformationRequirement]s with JSON.
* For the default implementation see [CobblemonRequirementAdapter].
*
* @author Licious
* @since March 21st, 2022
*/
interface RequirementAdapter : JsonDeserializer<TransformationRequirement>, JsonSerializer<TransformationRequirement> {
/**
* Registers the given type of [TransformationRequirement] to it's associated ID for deserialization.
*
* @param T The type of [TransformationRequirement].
* @param id The id of the evolution event.
* @param type The [KClass] of the type.
*/
fun <T : TransformationRequirement> registerType(id: String, type: KClass<T>)
}
@@ -0,0 +1,35 @@
/*
* 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.api.pokemon.transformation.adapters
import com.cobblemon.mod.common.api.pokemon.transformation.trigger.TransformationTrigger
import com.cobblemon.mod.common.pokemon.transformation.adapters.CobblemonTriggerAdapter
import com.google.gson.JsonDeserializer
import com.google.gson.JsonSerializer
import kotlin.reflect.KClass
/**
* Saves and loads [TransformationTrigger]s with JSON.
* For the default implementation see [CobblemonTriggerAdapter].
*
* @author Licious
* @since March 20th, 2022
*/
interface TriggerAdapter : JsonDeserializer<TransformationTrigger>, JsonSerializer<TransformationTrigger> {
/**
* Registers the given type of [TransformationTrigger] to it's associated ID for deserialization.
*
* @param T The type of [TransformationTrigger].
* @param id The id of the evolution event.
* @param type The [KClass] of the type.
*/
fun <T : TransformationTrigger> registerType(id: String, type: KClass<T>)
}
@@ -0,0 +1,106 @@
/*
* 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.api.pokemon.transformation.controller
import com.cobblemon.mod.common.api.pokemon.transformation.TransformationLike
import com.cobblemon.mod.common.api.pokemon.transformation.proxy.PreProcessor
import com.cobblemon.mod.common.api.pokemon.transformation.progress.TransformationProgress
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.controller.CobblemonClientTransformationController
import com.cobblemon.mod.common.pokemon.transformation.controller.CobblemonServerTransformationController
/**
* Responsible for holding all available [TransformationLike]s in the [Pokemon].
* Also handles all the networking behind them.
* For the Cobblemon default implementations see [CobblemonClientTransformationController] & [CobblemonServerTransformationController].
*
* @author Licious
* @since April 28th, 2022
*/
interface TransformationController<T : TransformationLike, out I : PreProcessor> : MutableSet<T> {
/**
* Resolves the [Pokemon] attached to this controller.
*
* @return The [Pokemon] this controller is attached to.
*/
fun pokemon(): Pokemon
/**
* Starts the given transformation on this controller.
* All the necessary networking will be handled.
*
* @param transformation The [TransformationLike] starting.
*/
fun start(transformation: T)
/** Reverts the active [TransformationLike] for this controller's [pokemon]. */
fun revert() // TODO dunno if i like this
/**
* Returns an immutable collection of [TransformationProgress]es attached to this controller.
*
* @return The currently tracked [TransformationProgress]es.
*/
fun progress(): Collection<TransformationProgress<*>>
/**
* Attaches a [TransformationProgress] to this controller.
*
* @param P The type of [TransformationProgress].
* @param progress The [TransformationProgress] being attached for tracking.
* @return The instance being tracked.
*/
fun <P : TransformationProgress<*>> trackProgress(progress: P): P
/**
* Attempts to find an [TransformationProgress] in [progress] or creates and tracks using [trackProgress].
*
* @param P The type of [TransformationProgress].
* @param predicate The lambda attempting to find specific progress. This needs to ensure the [TransformationProgress] is of type [P] or the cast will fail.
* @param progressFactory The lambda creating a new instance of type [P] if [predicate] can't find an element.
* @return The found match in [progress] or the created and now tracked with [trackProgress].
*/
fun <P : TransformationProgress<*>> progressFirstOrCreate(predicate: (progress: TransformationProgress<*>) -> Boolean, progressFactory: () -> P): P
fun asIntermediate(): I
/**
* Guard for [start] to prevent a transformation from executing while the [pokemon] is busy or if the transformation
* is not in the controller.
*
* @param transformation the [TransformationLike] to validate.
* @return whether this controller is capable of starting the transformation.
*/
fun canStart(transformation: T): Boolean
/**
* Whether this controller is capable of executing [start] for a transformation. This checks if the [pokemon] is busy
* or if there is a transformation ready in the controller.
*
* @return whether this controller is capable of starting a transformation.
*/
fun canStart(): Boolean
/**
* Validates the [TransformationLike] currently active for this controller's [pokemon] and determines whether it can be
* reverted and if the pokemon is capable of reverting right now.
*
* @return whether the transformation can be reverted.
*/
fun canRevert(): Boolean
/**
* Retrieves the [TransformationLike] in this controller that matches the provided id.
*
* @param id the identifier of the transformation.
* @return the transformation matching this id.
*/
fun getById(id: String) = this.firstOrNull { transformation -> transformation.id.equals(id, true) }
}
@@ -6,11 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.pokemon.evolution
package com.cobblemon.mod.common.api.pokemon.transformation.evolution
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.pokemon.evolution.StandardPreEvolution
import com.cobblemon.mod.common.pokemon.transformation.evolution.StandardPreEvolution
/**
* Represents the previous stage in the evolutionary line of a given Pokémon.
@@ -6,25 +6,36 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.pokemon.evolution.progress
package com.cobblemon.mod.common.api.pokemon.transformation.progress
import com.cobblemon.mod.common.api.data.Identifiable
import com.cobblemon.mod.common.api.pokemon.evolution.EvolutionController
import com.cobblemon.mod.common.api.pokemon.transformation.controller.TransformationController
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.Pokemon
import com.google.gson.JsonObject
import net.minecraft.nbt.CompoundTag
import net.minecraft.resources.ResourceLocation
/**
* Represents a container of progression towards the completion of an evolution requirement.
* To register an evolution progress use [EvolutionProgressTypes.registerType].
* Represents a container of progression towards the completion of a [TransformationRequirement].
* To register a transformation progress use [TransformationProgressTypes.registerType].
*
* @param T The type of the progression.
*
* @author Licious
* @since January 27th, 2023
*/
interface EvolutionProgress<T> : Identifiable {
interface TransformationProgress<T : TransformationProgress.Progress> : Identifiable {
/**
* Represents progression towards a [TransformationRequirement].
*
* @property amount The value required to be complete.
*
* @author Segfault Guy
* @since April 18th, 2025
*/
interface Progress {
val amount: Int
}
/**
* Gets the current progress of type [T].
@@ -47,7 +58,7 @@ interface EvolutionProgress<T> : Identifiable {
fun reset()
/**
* Used to decide if the given progress should be kept in the [pokemon]s [EvolutionController].
* Used to decide if the given progress should be kept in the [pokemon]s [TransformationController].
*
* @param pokemon The [Pokemon] being queried.
* @return True if the progress is still required otherwise false.
@@ -55,16 +66,16 @@ interface EvolutionProgress<T> : Identifiable {
fun shouldKeep(pokemon: Pokemon): Boolean
/**
* The [EvolutionProgressType] of this.
* The [TransformationProgressType] of this.
*
* @return The [EvolutionProgressType] of this.
* @return The [TransformationProgressType] of this.
*/
fun type(): EvolutionProgressType<*>
fun type(): TransformationProgressType<*>
companion object {
@JvmStatic
fun codec() = EvolutionProgressTypes.codec()
fun codec() = TransformationProgressTypes.codec()
}
}
@@ -6,10 +6,10 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.pokemon.evolution.progress
package com.cobblemon.mod.common.api.pokemon.transformation.progress
import com.mojang.serialization.MapCodec
data class EvolutionProgressType<T : EvolutionProgress<*>>(
data class TransformationProgressType<T : TransformationProgress<*>>(
val codec: MapCodec<T>
)
@@ -0,0 +1,72 @@
/*
* 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.api.pokemon.transformation.progress
import com.cobblemon.mod.common.pokemon.transformation.progress.*
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.serialization.Codec
import com.mojang.serialization.Lifecycle
import net.minecraft.core.MappedRegistry
import net.minecraft.core.Registry
import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation
import kotlin.jvm.Throws
/**
* Registry of [TransformationProgressType]s.
*
* @author Licious
* @since July 6th, 2024
*/
object TransformationProgressTypes {
private const val ID_KEY = "id"
private val REGISTRY: Registry<TransformationProgressType<*>> = MappedRegistry(
ResourceKey.createRegistryKey(cobblemonResource("transformation_progress")), Lifecycle.stable()
)
@JvmStatic
val DAMAGE_TAKEN = this.registerType(DamageTakenTransformationProgress.ID, TransformationProgressType(DamageTakenTransformationProgress.CODEC))
@JvmStatic
val DEFEAT = this.registerType(DefeatTransformationProgress.ID, TransformationProgressType(DefeatTransformationProgress.CODEC))
@JvmStatic
val LAST_BATTLE_CRITICAL_HITS = this.registerType(LastBattleCriticalHitsTransformationProgress.ID, TransformationProgressType(LastBattleCriticalHitsTransformationProgress.CODEC))
@JvmStatic
val RECOIL = this.registerType(RecoilTransformationProgress.ID, TransformationProgressType(RecoilTransformationProgress.CODEC))
@JvmStatic
val USE_MOVE = this.registerType(UseMoveTransformationProgress.ID, TransformationProgressType(UseMoveTransformationProgress.CODEC))
/**
* Registers a transformation progress.
*
* @param T The type of the [TransformationProgress].
* @param id The [ResourceLocation].
* @param type The [TransformationProgressType] record.
* @return The registered [TransformationProgressType] of type [T].
*
* @throws [IllegalStateException] if [Registry.register] fails.
*/
@Throws(IllegalStateException::class)
@JvmStatic
fun <T : TransformationProgress<*>> registerType(id: ResourceLocation, type: TransformationProgressType<T>): TransformationProgressType<T> {
return Registry.register(REGISTRY, id, type)
}
/**
* Creates a codec instance of this registry.
*
* @return The generated [Codec] of [TransformationProgress].
*/
fun codec(): Codec<TransformationProgress<*>> = REGISTRY.byNameCodec().dispatch(
ID_KEY,
{ it.type() },
{ it.codec }
)
}
@@ -6,13 +6,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.pokemon.evolution
package com.cobblemon.mod.common.api.pokemon.transformation.proxy
import com.cobblemon.mod.common.api.pokemon.transformation.controller.TransformationController
import com.cobblemon.mod.common.pokemon.Pokemon
/**
* TODO
*/
fun interface PreProcessor {
fun create(pokemon: Pokemon): EvolutionController<*, *>
fun create(pokemon: Pokemon): TransformationController<*, *>
}
@@ -0,0 +1,64 @@
/*
* 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.api.pokemon.transformation.proxy
import com.cobblemon.mod.common.api.pokemon.transformation.Transformation
import com.cobblemon.mod.common.api.pokemon.transformation.TransformationDisplay
import com.cobblemon.mod.common.api.pokemon.transformation.TransformationLike
import com.cobblemon.mod.common.api.pokemon.transformation.controller.TransformationController
/**
* Responsible for holding the different [TransformationController] implementations based on the logical side.
* If you haven't already checked your side previously you can use [isClient].
*
* @param C The type of [TransformationDisplay] on the client side.
* @param S The type of [Transformation] on the server side.
* @param CI The type of [PreProcessor] on the client side.
* @param SI The type of [PreProcessor] on the server side.
*
* @author Licious, Segfault Guy
* @since January 19th, 2025
*/
interface TransformationProxy<C : TransformationDisplay, S : Transformation, CI : PreProcessor, SI : PreProcessor> {
/**
* Checks if the current logical side is the client.
*
* @return True if the current logical side is the client.
*/
fun isClient(): Boolean
/**
* Returns the current [TransformationController].
* Keep in mind this will have type erasure and as such most operations will not be possible.
* It's recommended to work with [client] or [server] when possible.
*
* @return The current [TransformationController].
*/
fun current(): TransformationController<out TransformationLike, *>
/**
* The client side implementation of the [TransformationController].
*
* @throws [ClassCastException] if called from the server side.
*
* @return The client side implementation of the [TransformationController] if possible.
*/
fun client(): TransformationController<C, CI>
/**
* The server side implementation of the [TransformationController].
*
* @throws [ClassCastException] if called from the client side.
*
* @return The server side implementation of the [TransformationController] if possible.
*/
fun server(): TransformationController<S, SI>
}
@@ -6,23 +6,22 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokemon.evolution.requirements.template
package com.cobblemon.mod.common.api.pokemon.transformation.requirement
import com.cobblemon.mod.common.api.pokemon.evolution.requirement.EvolutionRequirement
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.world.entity.LivingEntity
/**
* An [EvolutionRequirement] that expects a [LivingEntity] to be attached to the [Pokemon].
* A [TransformationRequirement] that expects a [LivingEntity] to be attached to the [Pokemon].
* It can be the [PokemonEntity] if present or the [LivingEntity] that owns it.
*
* Whenever an entity can't be resolved [EvolutionRequirement.check] will never succeed.
* Whenever an entity can't be resolved [TransformationRequirement.check] will never succeed.
*
* @author Licious
* @since March 21st, 2022
*/
interface EntityQueryRequirement : EvolutionRequirement {
interface EntityQueryRequirement : TransformationRequirement {
override fun check(pokemon: Pokemon): Boolean {
val queriedEntity = pokemon.entity ?: pokemon.getOwnerPlayer() ?: return false
return this.check(pokemon, queriedEntity)
@@ -32,7 +31,7 @@ interface EntityQueryRequirement : EvolutionRequirement {
* Checks if the given [Pokemon] & [LivingEntity] satisfies the requirement.
*
* @param pokemon The [Pokemon] being queried.
* @param queriedEntity The [LivingEntity] that can be compared in context of a [Level].
* @param queriedEntity The [LivingEntity] that can be compared in context of a [net.minecraft.world.level.Level].
* @return If the requirement was satisfied.
*/
fun check(pokemon: Pokemon, queriedEntity: LivingEntity): Boolean
@@ -0,0 +1,57 @@
/*
* 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.api.pokemon.transformation.requirement
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.server.level.ServerPlayer
/**
* A [TransformationRequirement] that expects the [Pokemon] to have an owner.
* It can be the [ServerPlayer] or the [NPCEntity] that owns it.
*
* Whenever an owner can't be resolved [TransformationRequirement.check] will never succeed.
*
* @author Polymeta
* @since May 6th, 2025
*/
interface OwnerQueryRequirement : TransformationRequirement {
override fun check(pokemon: Pokemon): Boolean {
val owner = pokemon.getOwnerEntity() ?: return false
return when (owner) {
is ServerPlayer -> {
this.checkPlayer(pokemon, owner)
}
is NPCEntity -> {
this.checkNPC(pokemon, owner)
}
else -> false
}
}
/**
* Checks if the given [Pokemon] & [ServerPlayer] satisfies the requirement.
*
* @param pokemon The [Pokemon] being queried.
* @param owner The [ServerPlayer] that owns this [Pokemon].
* @return If the requirement was satisfied.
*/
fun checkPlayer(pokemon: Pokemon, owner: ServerPlayer): Boolean
/**
* Checks if the given [Pokemon] & [NPCEntity] satisfies the requirement.
* This method mainly exists as NPCs are different to players in a lot of ways (e.g., lack of advancements),
* so they need to be handled differently, either by checking an NPC config or always returning true/false
*
* @param pokemon The [Pokemon] being queried.
* @param owner The [NPCEntity] that owns this [Pokemon].
* @return If the requirement was satisfied.
*/
fun checkNPC(pokemon: Pokemon, owner: NPCEntity): Boolean
}
@@ -6,23 +6,22 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.pokemon.evolution.requirement
package com.cobblemon.mod.common.api.pokemon.transformation.requirement
import com.cobblemon.mod.common.api.pokemon.evolution.ContextEvolution
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.transformation.Transformation
import com.cobblemon.mod.common.api.pokemon.transformation.trigger.ContextTrigger
import com.cobblemon.mod.common.pokemon.Pokemon
/**
* Represents a requirement for an evolution.
* Represents a requirement for a transformation.
* Requirements are constant and will never change regardless of the backing action.
* For dynamic requirements see [ContextEvolution].
*
* See [Evolution.requirements] & [Evolution.test] for usage.
* See [Transformation.requirements] & [Transformation.test] for usage.
*
* @author Licious
* @since March 19th, 2022
*/
fun interface EvolutionRequirement {
fun interface TransformationRequirement {
/**
* Checks if the given [Pokemon] satisfies the requirement.
@@ -32,4 +31,10 @@ fun interface EvolutionRequirement {
*/
fun check(pokemon: Pokemon): Boolean
/**
* Callback after the [Transformation] has been performed.
*
* @param pokemon The [Pokemon] being queried.
*/
fun fulfill(pokemon: Pokemon) {}
}
@@ -0,0 +1,55 @@
/*
* 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.api.pokemon.transformation.trigger
import com.cobblemon.mod.common.api.pokemon.transformation.Transformation
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.triggers.ItemInteractionTrigger
import com.cobblemon.mod.common.pokemon.transformation.triggers.TradeTrigger
/**
* Represents a [TransformationTrigger] of a [Pokemon] that can only occur during specific actions and with added context.
* For the default implementations see [ItemInteractionTrigger] & [TradeTrigger].
*
* @param TC The context that is serialized from JSON during species loading. This is what the [Context] provided
* to [testTrigger] is expected to match against.
*
* @author Licious
* @since March 19th, 2022
*/
interface ContextTrigger<TC>: TransformationTrigger {
/**
* Represents context provided for [TransformationTrigger] evaluation.
*
* @author Segfault Guy
* @since October 15th, 2023
*/
interface Context
/** The target context required for this [Transformation] to be successfully triggered. */
val requiredContext: TC
/**
* Checks if the given context is valid for the [requiredContext].
*
* @param context The [Context] given at runtime when querying the [Transformation].
* @return If the context matched the [requiredContext].
*/
fun testContext(context: Context): Boolean = false
/**
* Evaluates the triggering condition of a [Transformation].
*
* @param context The [Context] of the [Transformation].
* @return Whether the triggering condition is satisfied. If [context] is null, bypasses the condition check.
*/
override fun testTrigger(context: Context?) = context?.let { testContext(it) } ?: true
}
@@ -0,0 +1,23 @@
/*
* 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.api.pokemon.transformation.trigger
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.triggers.LevelUpTrigger
/**
* Represents a [TransformationTrigger] of a [Pokemon] that is attempted periodically without any additional context or actions.
* For the default implementation see [LevelUpTrigger].
*
* Triggers that implement this marker will be tested every second.
*
* @author Licious
* @since March 19th, 2022
*/
interface PassiveTrigger : TransformationTrigger
@@ -0,0 +1,29 @@
/*
* 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.api.pokemon.transformation.trigger
import com.cobblemon.mod.common.api.pokemon.transformation.Transformation
/**
* Represents a condition that starts a [Transformation].
*
* @author Segfault Guy
* @since Sept 8th, 2023
*/
interface TransformationTrigger {
/**
* Evaluates the triggering condition of a [Transformation].
*
* @param context The optional [ContextTrigger.Context] for [ContextTrigger] evaluation.
* @return Whether the triggering condition is satisfied.
*/
fun testTrigger(context: ContextTrigger.Context?): Boolean = true
}
@@ -12,16 +12,14 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.advancement.CobblemonCriteria
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.pokemon.PokemonGainedEvent
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.evolution.PassiveEvolution
import com.cobblemon.mod.common.api.pokemon.transformation.trigger.PassiveTrigger
import com.cobblemon.mod.common.api.storage.pc.PCStore
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.pokemon.OriginalTrainerType
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.activestate.ShoulderedState
import com.cobblemon.mod.common.pokemon.evolution.variants.LevelUpEvolution
import com.cobblemon.mod.common.pokemon.transformation.form.TemporaryForm
import com.cobblemon.mod.common.util.*
import net.minecraft.nbt.CompoundTag
import net.minecraft.server.level.ServerPlayer
import net.minecraft.network.chat.Component
import java.util.*
@@ -143,14 +141,16 @@ open class PlayerPartyStore(
}
}
// Passive evolutions
pokemon.lockedEvolutions.filterIsInstance<PassiveEvolution>().forEach { it.attemptEvolution(pokemon) }
val removeList = mutableListOf<Evolution>()
pokemon.evolutionProxy.server().forEach {
if (!it.test(pokemon) && it is LevelUpEvolution && !it.permanent)
removeList.add(it)
}
removeList.forEach { pokemon.evolutionProxy.server().remove(it) }
// Passive evolutions and form changes
pokemon.triggerTransformations(PassiveTrigger::class.java)
// Reevaluate active form
val form = pokemon.form
if (form is TemporaryForm && !form.reevaluate(pokemon)) pokemon.formProxy.server().revert() // TODO check locks?
// Reevaluate transformation requirements and remove invalid transformations from the controller
pokemon.formProxy.server().reevaluate()
pokemon.evolutionProxy.server().reevaluate()
}
// Friendship
// ToDo expand this down the line just a very basic implementation for the first releases
@@ -127,7 +127,8 @@ data class MoveActionResponse(var moveName: String, var targetPnx: String? = nul
// No PP or disabled or something
return false
}
val availableTargets = (gimmickMove?.target ?: move.target).targetList(activeBattlePokemon)?.takeIf { it.isNotEmpty() } ?: return true
val availableTargets = (if (gimmickID != null && validGimmickMove) gimmickMove.target else move.target)
.targetList(activeBattlePokemon)?.takeIf { it.isNotEmpty() } ?: return true
val pnx = targetPnx ?: return false // If the targets list is non-null then they need to have specified a target
val (_, targetPokemon) = activeBattlePokemon.actor.battle.getActorAndActiveSlotFromPNX(pnx)
@@ -12,7 +12,6 @@ import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.battles.interpreter.*
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.api.text.red
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
@@ -22,9 +21,6 @@ import com.cobblemon.mod.common.battles.interpreter.instructions.*
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.runOnServer
import net.minecraft.world.level.ClipContext
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import java.util.UUID
import kotlin.collections.Iterator
import kotlin.collections.filter
@@ -36,7 +32,6 @@ import kotlin.collections.mutableMapOf
import kotlin.collections.set
import kotlin.collections.toMutableList
import kotlin.collections.toTypedArray
import kotlin.time.measureTime
@Suppress("KotlinPlaceholderCountMatchesArgumentCount", "UNUSED_PARAMETER")
object ShowdownInterpreter {
@@ -72,7 +67,7 @@ object ShowdownInterpreter {
updateInstructionParser["-copyboost"] = { _, _, message, _ -> CopyBoostInstruction(message) }
updateInstructionParser["-crit"] = { _, instructionSet, message, _ -> CritInstruction(message, instructionSet) }
updateInstructionParser["-curestatus"] = { _, _, message, _ -> CureStatusInstruction(message) }
updateInstructionParser["detailschange"] = { _, _, message, _ -> FormeChangeInstruction(message) }
updateInstructionParser["detailschange"] = { _, _, message, _ -> FormChangeInstruction(message) }
updateInstructionParser["-endability"] = { _, _, message, _ -> EndAbilityInstruction(message) }
updateInstructionParser["-end"] = { _, _, message, _ -> EndInstruction(message) }
updateInstructionParser["-enditem"] = { _, _, message, _ -> EndItemInstruction(message) }
@@ -81,7 +76,7 @@ object ShowdownInterpreter {
updateInstructionParser["-fieldactivate"] = { _, _, message, _ -> FieldActivateInstruction(message) }
updateInstructionParser["-fieldend"] = { _, _, message, _ -> FieldEndInstruction(message) }
updateInstructionParser["-fieldstart"] = { _, _, message, _ -> FieldStartInstruction(message) }
updateInstructionParser["-formechange"] = { _, _, message, _ -> FormeChangeInstruction(message) }
updateInstructionParser["-formechange"] = { _, _, message, _ -> FormChangeInstruction(message) }
updateInstructionParser["-hitcount"] = { _, _, message, _ -> HitCountInstruction(message) }
updateInstructionParser["-immune"] = { _, _, message, _ -> ImmuneInstruction(message) }
updateInstructionParser["-invertboost"] = { _, _, message, _ -> InvertBoostInstruction(message) }
@@ -12,10 +12,10 @@ 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
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.pokemon.transformation.progress.LastBattleCriticalHitsTransformationProgress
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-crit|POKEMON
@@ -39,9 +39,10 @@ class CritInstruction(val message: BattleMessage, val instructionSet: Instructio
} else {
battle.broadcastChatMessage(battleLang("crit").yellow())
}
if (LastBattleCriticalHitsEvolutionProgress.supports(battlePokemon.effectedPokemon)) {
val progress = battlePokemon.effectedPokemon.evolutionProxy.current().progressFirstOrCreate({ it is LastBattleCriticalHitsEvolutionProgress }) { LastBattleCriticalHitsEvolutionProgress() }
progress.updateProgress(LastBattleCriticalHitsEvolutionProgress.Progress(progress.currentProgress().amount + 1))
if (LastBattleCriticalHitsTransformationProgress.supports(battlePokemon.effectedPokemon)) {
// TODO refactor and apply to forms
val progress = battlePokemon.effectedPokemon.evolutionProxy.current().progressFirstOrCreate({ it is LastBattleCriticalHitsTransformationProgress }) { LastBattleCriticalHitsTransformationProgress() }
progress.updateProgress(LastBattleCriticalHitsTransformationProgress.Progress(progress.currentProgress().amount + 1))
}
}
battle.minorBattleActions[pokemon.uuid] = message
@@ -25,10 +25,9 @@ import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.animation.PlayPosableAnimationPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
import com.cobblemon.mod.common.pokemon.evolution.progress.DamageTakenEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.RecoilEvolutionProgress
import com.cobblemon.mod.common.pokemon.status.statuses.persistent.PoisonStatus
import com.cobblemon.mod.common.pokemon.transformation.progress.DamageTakenTransformationProgress
import com.cobblemon.mod.common.pokemon.transformation.progress.RecoilTransformationProgress
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
@@ -75,7 +74,7 @@ class DamageInstruction(
private fun doRecoilEvoChecks(battlePokemon: BattlePokemon) {
battlePokemon.effectedPokemon.let { pokemon ->
if (RecoilEvolutionProgress.supports(pokemon)) {
if (RecoilTransformationProgress.supports(pokemon)) {
val healthStr = privateMessage.argumentAt(1) ?: throw UnsupportedOperationException(
"Cant get recoil string"
)
@@ -84,9 +83,10 @@ class DamageInstruction(
?: throw UnsupportedOperationException("Cant get recoil string")
val difference = pokemon.currentHealth - newHealth
if (difference > 0) {
// TODO refactor and apply to forms
val progress = pokemon.evolutionProxy.current()
.progressFirstOrCreate({ it is RecoilEvolutionProgress }) { RecoilEvolutionProgress() }
progress.updateProgress(RecoilEvolutionProgress.Progress(progress.currentProgress().recoil + difference))
.progressFirstOrCreate({ it is RecoilTransformationProgress }) { RecoilTransformationProgress() }
progress.updateProgress(RecoilTransformationProgress.Progress(progress.currentProgress().amount + difference))
}
}
}
@@ -180,9 +180,10 @@ class DamageInstruction(
battlePokemon.effectedPokemon.currentHealth = remainingHealth
if (difference > 0) {
battlePokemon.effectedPokemon.let { pokemon ->
if (DamageTakenEvolutionProgress.supports(pokemon)) {
val progress = pokemon.evolutionProxy.current().progressFirstOrCreate({ it is DamageTakenEvolutionProgress }) { DamageTakenEvolutionProgress() }
progress.updateProgress(DamageTakenEvolutionProgress.Progress(progress.currentProgress().amount + difference))
if (DamageTakenTransformationProgress.supports(pokemon)) {
// TODO refactor and apply to forms
val progress = pokemon.evolutionProxy.current().progressFirstOrCreate({ it is DamageTakenTransformationProgress }) { DamageTakenTransformationProgress() }
progress.updateProgress(DamageTakenTransformationProgress.Progress(progress.currentProgress().amount + difference))
}
}
}
@@ -0,0 +1,108 @@
/*
* 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.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.ActionEffectInstruction
import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.battle.BattleTransformPokemonPacket
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.transformation.form.TemporaryForm
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.concurrent.CompletableFuture
/**
* Format: |-formechange|POKEMON|SPECIES|HP STATUS or |detailschange|POKEMON|DETAILS|HP STATUS
*
* POKEMON has temporarily (-formechange) or permanently (detailschange) changed form to SPECIES/DETAILS.
* @author Segfault Guy
* @since December 9th, 2024
*/
class FormChangeInstruction(val message: BattleMessage): ActionEffectInstruction {
override var future: CompletableFuture<*> = CompletableFuture.completedFuture(Unit)
override var holds = mutableSetOf<String>()
override val id = cobblemonResource("formchange")
lateinit var battlePokemon: BattlePokemon
lateinit var speciesName: String
lateinit var formId: String
lateinit var typeKey: String
lateinit var pnx: String
lateinit var form: FormData
override fun preActionEffect(battle: PokemonBattle) {
val details = message.argumentAt(1)?.split(',')?.get(0)?.lowercase() ?: return
battlePokemon = message.battlePokemon(0, battle) ?: return
speciesName = details.substringBefore('-')
formId = details.substringAfter('-').ifBlank { speciesName }
typeKey = if (message.id == "detailschange") "permanent" else "temporary"
pnx = message.pnxAndUuid(0)?.first ?: return
ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), battlePokemon)
battle.dispatchGo {
battle.minorBattleActions[battlePokemon.uuid] = message
form = battlePokemon.effectedPokemon.species.getFormByShowdownId(formId)
CobblemonEvents.FORME_CHANGE.post(FormeChangeEvent(battle, battlePokemon, formId))
}
}
override fun runActionEffect(battle: PokemonBattle, runtime: MoLangRuntime) {
battle.dispatch {
if (form is TemporaryForm) {
val actionEffect = (form as TemporaryForm).actionEffect
val providers = mutableListOf<Any>(battle)
val pokemon = message.battlePokemon(0, battle) ?: return@dispatch GO
pokemon.effectedPokemon.entity?.let { UsersProvider(it) }?.let(providers::add)
val context = ActionEffectContext(
actionEffect = actionEffect,
runtime = runtime,
providers = providers,
level = battle.players.firstOrNull()?.level()
)
(form as TemporaryForm).forceStart(battlePokemon.effectedPokemon.entity!!, context)
}
else {
battlePokemon.effectedPokemon.formProxy.server().revert()
}
return@dispatch GO
}
}
override fun postActionEffect(battle: PokemonBattle) {
battle.dispatchWaiting {
CobblemonEvents.FORME_CHANGE.post(FormeChangeEvent(battle, battlePokemon, formId))
val pokemonName = battlePokemon.getName()
val lang = when (formId) {
"busted", "hero", "complete" -> return@dispatchWaiting
"school", "wishiwashi", "meteor", "minior" -> battleLang("formechange.$formId", pokemonName)
speciesName -> battleLang("formechange.default.temporary.end", pokemonName)
else -> battleLang("formechange.default.$typeKey", pokemonName, formId)
}
battle.sendSidedUpdate(
source = battlePokemon.actor,
allyPacket = BattleTransformPokemonPacket(pnx, battlePokemon, true),
opponentPacket = BattleTransformPokemonPacket(pnx, battlePokemon, false)
)
battle.broadcastChatMessage(lang)
}
}
}
@@ -1,51 +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.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format: |-formechange|POKEMON|SPECIES|HP STATUS or |detailschange|POKEMON|DETAILS|HP STATUS
*
* POKEMON has temporarily (-formechange) or permanently (detailschange) changed form to SPECIES/DETAILS.
* @author Segfault Guy
* @since December 9th, 2024
*/
class FormeChangeInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val battlePokemon = message.battlePokemon(0, battle) ?: return
val details = message.argumentAt(1)?.split(',')?.get(0)?.lowercase() ?: return
val speciesName = details.substringBefore('-')
val formName = details.substringAfterLast('-').ifBlank { speciesName }
val typeKey = if (message.id == "detailschange") "permanent" else "temporary"
ShowdownInterpreter.broadcastOptionalAbility(battle, message.effect(), battlePokemon)
battle.dispatchWaiting {
battle.minorBattleActions[battlePokemon.uuid] = message
CobblemonEvents.FORME_CHANGE.post(FormeChangeEvent(battle, battlePokemon, formName))
val pokemonName = battlePokemon.getName()
val lang = when(formName) {
"busted", "hero", "complete" -> return@dispatchWaiting
"school", "wishiwashi", "meteor", "minior" -> battleLang("formechange.$formName", pokemonName)
speciesName -> battleLang("formechange.default.temporary.end", pokemonName)
else -> battleLang("formechange.default.$typeKey", pokemonName, formName)
}
battle.broadcastChatMessage(lang)
}
}
}
@@ -26,7 +26,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.pokemon.BattlePokemon
import com.cobblemon.mod.common.pokemon.evolution.progress.UseMoveEvolutionProgress
import com.cobblemon.mod.common.pokemon.transformation.progress.UseMoveTransformationProgress
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.concurrent.CompletableFuture
@@ -70,9 +70,10 @@ class MoveInstruction(
val spreadTargetPokemon = spreadTargets.map { battle.activePokemon.firstOrNull() { poke -> poke.getPNX() == it }?.battlePokemon }
userPokemon.effectedPokemon.let { pokemon ->
if (UseMoveEvolutionProgress.supports(pokemon, move)) {
val progress = pokemon.evolutionProxy.current().progressFirstOrCreate({ it is UseMoveEvolutionProgress && it.currentProgress().move == move }) { UseMoveEvolutionProgress() }
progress.updateProgress(UseMoveEvolutionProgress.Progress(move, progress.currentProgress().amount + 1))
// TODO refactor and apply to forms
if (UseMoveTransformationProgress.supports(pokemon, move)) {
val progress = pokemon.evolutionProxy.current().progressFirstOrCreate({ it is UseMoveTransformationProgress && it.currentProgress().move == move }) { UseMoveTransformationProgress() }
progress.updateProgress(UseMoveTransformationProgress.Progress(move, progress.currentProgress().amount + 1))
}
}
@@ -27,8 +27,8 @@ import com.cobblemon.mod.common.pokemon.Nature
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.properties.BattleCloneProperty
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.transformation.form.TemporaryForm
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.server
import java.util.UUID
import net.minecraft.network.chat.MutableComponent
import java.util.function.Function
@@ -42,10 +42,12 @@ open class BattlePokemon(
companion object {
fun safeCopyOf(pokemon: Pokemon): BattlePokemon {
//TOOD figure out a closer registry access (might have to break some method signatures for this (1.7?)
val effectedPokemon = pokemon.clone(registryAccess = server()?.registryAccess() ?: throw IllegalStateException("No registry access available"))
val effectedPokemon = pokemon.clone()
BattleCloneProperty.isBattleClone().apply(effectedPokemon)
UncatchableProperty.uncatchable().apply(effectedPokemon)
val shouldRevertForm = (effectedPokemon.form as? TemporaryForm)?.revertBeforeBattle ?: (effectedPokemon.form as? TemporaryForm)?.battleOnly ?: false
if (shouldRevertForm)
effectedPokemon.formProxy.server().revert()
return BattlePokemon(
originalPokemon = pokemon,
effectedPokemon = effectedPokemon,
@@ -53,13 +55,18 @@ open class BattlePokemon(
)
}
fun playerOwned(pokemon: Pokemon): BattlePokemon = BattlePokemon(
originalPokemon = pokemon,
effectedPokemon = pokemon,
postBattleEntityOperation = { entity ->
entity.effects.wipe()
}
)
fun playerOwned(pokemon: Pokemon): BattlePokemon {
val shouldRevertForm = (pokemon.form as? TemporaryForm)?.revertBeforeBattle ?: (pokemon.form as? TemporaryForm)?.battleOnly ?: false
if (shouldRevertForm)
pokemon.formProxy.server().revert()
return BattlePokemon(
originalPokemon = pokemon,
effectedPokemon = pokemon,
postBattleEntityOperation = { entity ->
entity.effects.wipe()
}
)
}
}
val struct = QueryStruct(
@@ -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 { permForm ->
jsArray.setArrayElement(index++, this.gson.toJson(PokemonSpecies.ShowdownPermanentForm(permForm)))
permForm.changeableForms.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 { permForm ->
sendSpeciesData(PokemonSpecies.ShowdownPermanentForm(permForm))
permForm.changeableForms.forEach { tempForm ->
sendSpeciesData(PokemonSpecies.ShowdownTemporaryForm(tempForm))
}
}
species.temporaryForms.forEach { form ->
sendSpeciesData(PokemonSpecies.ShowdownTemporaryForm(form))
}
}
}
@@ -344,4 +344,6 @@ object CobblemonClient {
}
}
/** Checks if the player is preoccupied and cannot accept an action. */
fun isBusy() = battle != null || trade != null
}
@@ -14,7 +14,9 @@ import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.ForcePassActionResponse
import com.cobblemon.mod.common.battles.MoveActionResponse
import com.cobblemon.mod.common.battles.PassActionResponse
import com.cobblemon.mod.common.battles.ShowdownMoveset
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
import java.util.UUID
@@ -80,6 +82,10 @@ class ClientBattle(
}
}
fun pendingGimmick(gimmick: ShowdownMoveset.Gimmick): Boolean {
return pendingActionRequests.none { (it.response as? MoveActionResponse)?.gimmickID == gimmick.id }
}
fun getPokemonFromPNX(pnx: String): Pair<ClientBattleActor, ActiveClientBattlePokemon> {
val actor = sides.flatMap { it.actors }.find { it.showdownId == pnx.substring(0, 2) }
?: throw IllegalStateException("Invalid pnx: $pnx - unknown actor")
@@ -104,6 +104,11 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
// force a model update - handles edge case where the PosableState's tracked PosableModel isn't updated until the LivingEntityRenderer render is run
currentModel = VaryingModelRepository.getPoser(identifier, this)
currentEntity.refreshRiding()
}
else if (data == PokemonEntity.FORM) {
val formId = currentEntity.entityData.get(PokemonEntity.FORM)
currentEntity.pokemon.form = currentEntity.pokemon.species.getFormByShowdownId(formId)
currentEntity.refreshRiding()
} else if (data == PokemonEntity.ASPECTS) {
currentAspects = currentEntity.entityData.get(PokemonEntity.ASPECTS)
currentEntity.pokemon.shiny = currentAspects.contains("shiny")
@@ -13,11 +13,11 @@ import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleGimmickButton.GimmickTile
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.toRGB
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.sounds.SoundEvents
@@ -30,7 +30,7 @@ import net.minecraft.sounds.SoundEvents
* @author Segfault Guy
* @since July 8th, 2023
*/
abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Float, val y: Float) {
abstract class BattleGimmickButton(val gimmick: ShowdownMoveset.Gimmick, val x: Float, val y: Float) {
companion object {
const val WIDTH = 36
@@ -61,24 +61,32 @@ abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Floa
var toggled = false
private val sfx = SimpleSoundInstance.forUI(SoundEvents.ANVIL_LAND, 1.0F)
private val texture = gimmick.id
val selectable: Boolean
get() {
val battle = CobblemonClient.battle ?: return false
return battle.pendingGimmick(gimmick)
}
fun render(matrices: PoseStack, mouseX: Int, mouseY: Int, delta: Float) {
fun render(graphics: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
blitk(
matrixStack = matrices,
matrixStack = graphics.pose(),
texture = cobblemonResource("textures/gui/battle/battle_gimmick_${texture}.png"),
x = x * 2,
y = y * 2,
height = HEIGHT,
width = WIDTH,
vOffset = if (toggled || isHovered(mouseX.toDouble(), mouseY.toDouble())) HEIGHT else 0,
vOffset = if ((toggled || isHovered(mouseX.toDouble(), mouseY.toDouble()) && selectable)) HEIGHT else 0,
textureHeight = HEIGHT * 2,
scale = SCALE
scale = SCALE,
alpha = if (selectable) 1.0F else 0.5F
)
}
fun isHovered(mouseX: Double, mouseY: Double) = mouseX >= x && mouseX <= x + XOFF && mouseY >= y && mouseY <= y + YOFF
fun toggle(): Boolean {
if (!selectable)
return toggled
toggled = !toggled
Minecraft.getInstance().soundManager.play(sfx)
return toggled
@@ -74,7 +74,8 @@ class BattleMoveSelection(
BattleGimmickButton.create(gimmick, this, backButton.x + xOff, backButton.y)
}
val shiftButton = BattleShiftButton(x + 22.5F, Minecraft.getInstance().window.guiScaledHeight - 22F )
val shiftButton = if (request.activePokemon.getFormat().battleType.slotsPerActor == 3 && (request.activePokemon.getPNX()[2] == 'a' || request.activePokemon.getPNX()[2] == 'c'))
BattleShiftButton(x + 22.5F, Minecraft.getInstance().window.guiScaledHeight - 22F ) else null
open class MoveTile(
val moveSelection: BattleMoveSelection,
@@ -190,31 +191,30 @@ class BattleMoveSelection(
}
backButton.render(context, mouseX, mouseY, delta)
gimmickButtons.forEach {
it.render(context.pose(), mouseX, mouseY, delta)
}
if(this.request.activePokemon.getFormat().battleType.slotsPerActor == 3 && (request.activePokemon.getPNX()[2] == 'a' || request.activePokemon.getPNX()[2] == 'c')) {
shiftButton.render(context, mouseX, mouseY, delta)
it.render(context, mouseX, mouseY, delta)
}
shiftButton?.render(context, mouseX, mouseY, delta)
}
override fun mousePrimaryClicked(mouseX: Double, mouseY: Double): Boolean {
val move = moveTiles.find { it.isHovered(mouseX, mouseY) }
val gimmick = gimmickButtons.find { it.isHovered(mouseX, mouseY) }
if (move != null) {
if(this.request.activePokemon.getFormat().battleType.pokemonPerSide == 1) {
if (this.request.activePokemon.getFormat().battleType.pokemonPerSide == 1) {
move.onClick()
} else {
battleGUI.changeActionSelection(BattleTargetSelection(battleGUI, request, move.move))
return true
} else if (move.selectable) {
battleGUI.changeActionSelection(BattleTargetSelection(battleGUI, request, move.move, move.response.gimmickID, move.move.gimmickMove))
playDownSound(Minecraft.getInstance().soundManager)
return true
}
return true
} else if (backButton.isHovered(mouseX, mouseY)) {
playDownSound(Minecraft.getInstance().soundManager)
battleGUI.changeActionSelection(null)
} else if (gimmick != null) {
} else if (gimmick != null && gimmick.selectable) {
gimmickButtons.filter { it != gimmick }.forEach { it.toggled = false }
moveTiles = if (gimmick.toggle()) gimmick.tiles else baseTiles
} else if(shiftButton.isHovered(mouseX,mouseY)) {
} else if (shiftButton?.isHovered(mouseX,mouseY) == true) {
playDownSound(Minecraft.getInstance().soundManager)
battleGUI.selectAction(request, ShiftActionResponse())
}
@@ -14,6 +14,7 @@ import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.font
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.InBattleGimmickMove
import com.cobblemon.mod.common.battles.InBattleMove
import com.cobblemon.mod.common.battles.MoveActionResponse
import com.cobblemon.mod.common.client.CobblemonClient
@@ -22,7 +23,6 @@ import com.cobblemon.mod.common.client.battle.ActiveClientBattlePokemon
import com.cobblemon.mod.common.client.battle.SingleActionRequest
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleSwitchPokemonSelection.Companion
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
@@ -41,9 +41,11 @@ import kotlin.math.floor
import kotlin.math.sin
class BattleTargetSelection(
battleGUI: BattleGUI,
request: SingleActionRequest,
val move: InBattleMove
battleGUI: BattleGUI,
request: SingleActionRequest,
val move: InBattleMove,
val gimmickID: String?,
gimmickMove: InBattleGimmickMove?
) : BattleActionSelection(
battleGUI = battleGUI,
request = request,
@@ -80,10 +82,10 @@ class BattleTargetSelection(
}
val targets = request.activePokemon.getAllActivePokemon()
val targetType = if (gimmickID != null && gimmickMove != null) gimmickMove.target else move.target
val backButton = BattleBackButton(x + 9F, Minecraft.getInstance().window.guiScaledHeight - 22F)
val selectableTargetList = move.target.targetList(request.activePokemon)
val multiTargetList = if(selectableTargetList == null) request.activePokemon.getMultiTargetList(move.target) else null
val selectableTargetList = targetType.targetList(request.activePokemon)
val multiTargetList = if(selectableTargetList == null) request.activePokemon.getMultiTargetList(targetType) else null
val baseTiles = targets.mapIndexed { index, target ->
val isAlly = target.isAllied(request.activePokemon)
@@ -123,7 +125,7 @@ class BattleTargetSelection(
private val isMultiTarget = multiTargetList?.firstOrNull { it.getPNX() == target.getPNX() } != null
private val isCurrentPokemon = request.activePokemon.battlePokemon?.uuid == target.battlePokemon!!.uuid
open val response: MoveActionResponse get() = MoveActionResponse(targetSelection.move.id, responseTarget)
open val response: MoveActionResponse get() = MoveActionResponse(targetSelection.move.id, responseTarget, targetSelection.gimmickID)
val state = FloatingState()
open val selectable: Boolean get() = isMultiTarget || (responseTarget != null)
@@ -30,8 +30,11 @@ import com.cobblemon.mod.common.client.gui.summary.widgets.screens.marks.MarksWi
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.MoveSwapScreen
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.MovesWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.stats.StatWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.transformations.EvolutionTestSelectScreen
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.transformations.FormSelectScreen
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.net.messages.server.pokemon.update.form.RevertFormPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetItemHiddenPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.SwapPartyPokemonPacket
@@ -79,6 +82,7 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
const val PARTY = 0
const val MOVE_SWAP = 1
const val EVOLVE = 2
const val FORM = 3
// Resources
private val baseResource = cobblemonResource("textures/gui/summary/summary_base.png")
@@ -93,6 +97,8 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
private val tabIconMoves = cobblemonResource("textures/gui/summary/summary_tab_icon_moves.png")
private val tabIconStats = cobblemonResource("textures/gui/summary/summary_tab_icon_stats.png")
private val tabIconMarks = cobblemonResource("textures/gui/summary/summary_tab_icon_marks.png")
private val formButtonResource = cobblemonResource("textures/gui/summary/summary_form_test_button.png")
private val revertButtonResource = cobblemonResource("textures/gui/summary/summary_revert_test_button.png")
val iconShinyResource = cobblemonResource("textures/gui/summary/icon_shiny.png")
val iconHeldItemResource = cobblemonResource("textures/gui/summary/icon_item_held.png")
val iconCosmeticItemResource = cobblemonResource("textures/gui/summary/icon_item_cosmetic.png")
@@ -176,8 +182,46 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
},
text = lang("ui.evolve"),
resource = evolveButtonResource,
renderRequirement = { selectedPokemon.evolutionProxy.client().isNotEmpty() && CobblemonClient.battle == null },
clickRequirement = { selectedPokemon.evolutionProxy.client().isNotEmpty() && CobblemonClient.battle == null }
renderRequirement = { selectedPokemon.evolutionProxy.client().canStart() },
clickRequirement = { selectedPokemon.evolutionProxy.client().canStart() }
)
)
// TODO form and revert button are temp for debugging
// Form Button
addRenderableWidget(
SummaryButton(
buttonX = x + 4F,
buttonY = y + 30F,
buttonWidth = 10,
buttonHeight = 10,
clickAction = {
momentarily { displaySideScreen(if (sideScreenIndex == FORM) PARTY else FORM) }
},
resource = formButtonResource,
renderRequirement = { selectedPokemon.formProxy.client().canStart() },
clickRequirement = { selectedPokemon.formProxy.client().canStart() }
)
)
// Temporary form revert button
addRenderableWidget(
SummaryButton(
buttonX = x + 4F,
buttonY = y + 50F,
buttonWidth = 10,
buttonHeight = 10,
clickAction = {
momentarily {
Minecraft.getInstance().player?.clientSideCloseContainer()
playSound(CobblemonSounds.GUI_CLICK)
sendToServer(RevertFormPacket(selectedPokemon.uuid))
}
},
resource = revertButtonResource,
renderRequirement = { selectedPokemon.formProxy.client().canRevert() },
clickRequirement = { selectedPokemon.formProxy.client().canRevert() }
)
)
@@ -327,7 +371,7 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
this.party.getOrNull(newSelection)?.let { this.selectedPokemon = it }
listenToMoveSet()
displayMainScreen(mainScreenIndex)
children().find { it is EvolutionSelectScreen }?.let(this::removeWidget)
children().find { it is EvolutionTestSelectScreen }?.let(this::removeWidget)
if (::modelWidget.isInitialized) {
modelWidget.pokemon = selectedPokemon.asRenderablePokemon()
@@ -469,12 +513,22 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
EVOLVE -> {
saveActiveMark()
saveMarkings()
sideScreen = EvolutionSelectScreen(
sideScreen = EvolutionTestSelectScreen(
x + 216,
y + 23,
pokemon = selectedPokemon
)
}
FORM -> {
saveActiveMark()
saveMarkings()
sideScreen = FormSelectScreen(
x + 216,
y + 23,
pokemon = selectedPokemon
)
}
}
val element = sideScreen
if (element is Renderable && element is NarratableEntry) {
@@ -674,7 +728,7 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (sideScreenIndex == MOVE_SWAP || sideScreenIndex == EVOLVE) sideScreen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
if (sideScreenIndex == MOVE_SWAP || sideScreenIndex == EVOLVE || sideScreenIndex == FORM) sideScreen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
if (mainScreenIndex == MOVES) mainScreen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
@@ -19,7 +19,7 @@ import net.minecraft.client.gui.GuiGraphics
* @author Hiroku
* @since November 13th, 2023
*/
interface SummarySpeciesFeatureRenderer<T : SynchronizedSpeciesFeature> {
interface SummarySpeciesFeatureRenderer<T : SynchronizedSpeciesFeature<*>> {
/** The name of the feature (so we know where to look to find it in the [Pokemon]) */
val name: String
/** Draws it at a particular position. */
@@ -6,11 +6,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.summary.widgets
package com.cobblemon.mod.common.client.gui.summary.widgets.screens.transformations
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.evolution.EvolutionDisplay
import com.cobblemon.mod.common.api.pokemon.transformation.Transformation
import com.cobblemon.mod.common.api.pokemon.transformation.TransformationDisplay
import com.cobblemon.mod.common.api.pokemon.transformation.proxy.TransformationProxy
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.TypeIcon
@@ -22,23 +24,24 @@ import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.MutableComponent
import net.minecraft.sounds.SoundEvent
import org.joml.Quaternionf
import org.joml.Vector3f
class EvolutionSelectScreen(
abstract class EvolutionSelectScreen<C : TransformationDisplay, S : Transformation>(
x: Int,
y: Int,
val pokemon: Pokemon
): SummaryScrollList<EvolutionSelectScreen.EvolveSlot>(
lang: MutableComponent,
val pokemon: Pokemon,
): SummaryScrollList<EvolutionSelectScreen.TransformationSlot<C, S>>(
x,
y,
lang("ui.evolution"),
lang,
SLOT_HEIGHT + SLOT_SPACING
) {
companion object {
@@ -50,24 +53,30 @@ class EvolutionSelectScreen(
private val buttonResource = cobblemonResource("textures/gui/summary/summary_evolve_select_button.png")
}
abstract fun getSlots(pokemon: Pokemon): Collection<TransformationSlot<C, S>>
private var entriesCreated = false
public override fun addEntry(entry: EvolveSlot): Int {
public override fun addEntry(entry: TransformationSlot<C, S>): Int {
return super.addEntry(entry)
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) {
if (!entriesCreated) {
entriesCreated = true
pokemon.evolutionProxy.client().map { EvolveSlot(pokemon, it) }.forEach { entry -> this.addEntry(entry) }
getSlots(pokemon).forEach { entry -> this.addEntry(entry) }
}
super.renderWidget(context, mouseX, mouseY, partialTicks)
}
class EvolveSlot(private val pokemon: Pokemon, private val evolution: EvolutionDisplay) : Entry<EvolveSlot>() {
abstract class TransformationSlot<C : TransformationDisplay, S : Transformation>(
private val pokemon: Pokemon,
private val display: C,
text: MutableComponent
) : Entry<TransformationSlot<C, S>>() {
val client: Minecraft = Minecraft.getInstance()
val state = FloatingState()
val form: FormData = evolution.species.getForm(evolution.aspects)
val form: FormData = display.species.getForm(display.aspects)
val selectButton: SummaryButton = SummaryButton(
buttonX = 0F,
buttonY = 0F,
@@ -76,20 +85,22 @@ class EvolutionSelectScreen(
clickAction = {
Minecraft.getInstance().player?.clientSideCloseContainer()
playSound(CobblemonSounds.GUI_CLICK)
pokemon.evolutionProxy.client().start(this.evolution)
getProxy(pokemon).client().start(this.display)
},
text = lang("ui.evolve"),
text = text,
resource = buttonResource,
boldText = true,
largeText = false,
textScale = 0.5F
)
abstract fun getProxy(pokemon: Pokemon): TransformationProxy<C, S, *, *>
fun playSound(soundEvent: SoundEvent) {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(soundEvent, 1.0F))
}
override fun getNarration() = evolution.species.translatedName
override fun getNarration() = display.species.translatedName
override fun render(
context: GuiGraphics,
@@ -107,7 +118,7 @@ class EvolutionSelectScreen(
val y = rowTop
val matrices = context.pose()
state.currentAspects = evolution.aspects
state.currentAspects = display.aspects
blitk(
matrixStack = matrices,
@@ -121,7 +132,7 @@ class EvolutionSelectScreen(
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = evolution.species.translatedName.bold(),
text = display.species.translatedName.bold(),
x = x + 4,
y = y + 2,
shadow = true
@@ -146,7 +157,7 @@ class EvolutionSelectScreen(
matrices.translate(x + (PORTRAIT_DIAMETER / 2) + 65.0, y - 5.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
drawProfilePokemon(
species = this.evolution.species.resourceIdentifier,
species = this.display.species.resourceIdentifier,
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
@@ -0,0 +1,28 @@
package com.cobblemon.mod.common.client.gui.summary.widgets.screens.transformations
import com.cobblemon.mod.common.api.pokemon.transformation.proxy.TransformationProxy
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.cobblemon.mod.common.pokemon.transformation.evolution.EvolutionDisplay
import com.cobblemon.mod.common.util.lang
class EvolutionTestSelectScreen(
x: Int,
y: Int,
pokemon: Pokemon
) : EvolutionSelectScreen<EvolutionDisplay, Evolution>(x, y, lang("ui.evolution"), pokemon) {
override fun getSlots(pokemon: Pokemon): Collection<TransformationSlot<EvolutionDisplay, Evolution>> {
return pokemon.evolutionProxy.client().map { EvolveSlot(pokemon, it) }
}
class EvolveSlot(
pokemon: Pokemon,
display: EvolutionDisplay
) : TransformationSlot<EvolutionDisplay, Evolution>(pokemon, display, lang("ui.evolve")) {
override fun getProxy(pokemon: Pokemon): TransformationProxy<EvolutionDisplay, Evolution, *, *> {
return pokemon.evolutionProxy
}
}
}
@@ -0,0 +1,25 @@
package com.cobblemon.mod.common.client.gui.summary.widgets.screens.transformations
import com.cobblemon.mod.common.api.pokemon.transformation.proxy.TransformationProxy
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.FormDisplay
import com.cobblemon.mod.common.pokemon.transformation.form.TemporaryForm
import com.cobblemon.mod.common.util.lang
class FormSelectScreen(
x: Int,
y: Int,
pokemon: Pokemon
) : EvolutionSelectScreen<FormDisplay, TemporaryForm>(x, y, lang("ui.forms"), pokemon) {
override fun getSlots(pokemon: Pokemon): Collection<TransformationSlot<FormDisplay, TemporaryForm>> {
return pokemon.formProxy.client().map { FormSlot(pokemon, it) }
}
class FormSlot(pokemon: Pokemon, display: FormDisplay) : TransformationSlot<FormDisplay, TemporaryForm>(pokemon, display, lang("ui.form")) {
override fun getProxy(pokemon: Pokemon): TransformationProxy<FormDisplay, TemporaryForm, *, *> {
return pokemon.formProxy
}
}
}
@@ -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
}
}
@@ -26,6 +26,7 @@ import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.pokemon.Pokemon
@@ -67,12 +68,12 @@ object TestCommand {
try {
//this.testCodecOutput(context)
val player = context.source.entity as ServerPlayer
player.party().forEach { it.currentHealth = it.hp / 2 }
val npc = NPCEntity(player.level())
npc.setPos(player.x, player.y, player.z)
npc.npc = NPCClasses.getByName("standard")!!
npc.initialize(50)
player.level().addFreshEntity(npc)
//player.party().forEach { it.currentHealth = it.hp / 2 }
//val npc = NPCEntity(player.level())
//npc.setPos(player.x, player.y, player.z)
//npc.npc = NPCClasses.getByName("standard")!!
//npc.initialize(50)
//player.level().addFreshEntity(npc)
// val evolutionEntity = GenericBedrockEntity(world = player.level())
// evolutionEntity.apply {
// category = cobblemonResource("evolution")
@@ -126,6 +127,20 @@ object TestCommand {
// val player = context.source.entity as ServerPlayerEntity
// player.giveItemStack(PokemonItem.from(PokemonSpecies.random(), "alolan"))
val npcs = player.level().getEntities(player, player.boundingBox.inflate(20.0)) { it is NPCEntity }
.map { NPCBattleActor(it as NPCEntity, it.getPartyForChallenge(listOf(player))!!, 0) }
.sortedBy { player.distanceToSqr(it.entity) }
val playerTeam = player.party().toBattleTeam()
val playerActor = PlayerBattleActor(player.uuid, playerTeam)
BattleRegistry.startBattle(
battleFormat = BattleFormat.GEN_9_MULTI,
side1 = BattleSide(playerActor, npcs[0]),
side2 = BattleSide(npcs[1], npcs[2])
).ifSuccessful {
it.battlePartyStores.add(player.party())
}
} catch (e: Exception) {
Cobblemon.LOGGER.error(e)
}
@@ -401,7 +401,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
after(seconds = 0.1F) {
pokemon.beamMode = 0
pokemon.busyLocks.remove(this)
pokemon.busyLocks.remove(PokemonEntity.CAPTURE)
captureFuture.complete(false)
val ballType =
this.pokeBall.name.path.lowercase().replace("_", "")
@@ -424,7 +424,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
override fun getDimensions(pPose: Pose) = DIMENSIONS
private fun attemptCatch(pokemonEntity: PokemonEntity) {
pokemonEntity.busyLocks.add(this)
pokemonEntity.busyLocks.add(PokemonEntity.CAPTURE)
val displace = deltaMovement
captureState = CaptureState.HIT
val mul = if (random.nextBoolean()) 1 else -1
@@ -90,9 +90,9 @@ import com.cobblemon.mod.common.pokemon.activestate.ActivePokemonState
import com.cobblemon.mod.common.pokemon.activestate.InactivePokemonState
import com.cobblemon.mod.common.pokemon.activestate.ShoulderedState
import com.cobblemon.mod.common.pokemon.ai.FormPokemonBehaviour
import com.cobblemon.mod.common.pokemon.evolution.variants.ItemInteractionEvolution
import com.cobblemon.mod.common.pokemon.feature.StashHandler
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.feature.StashHandler
import com.cobblemon.mod.common.pokemon.transformation.triggers.ItemInteractionTrigger
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
import com.mojang.serialization.Codec
@@ -179,11 +179,14 @@ open class PokemonEntity(
@JvmStatic val FRIENDSHIP = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.INT)
@JvmStatic val FREEZE_FRAME = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic val CAUGHT_BALL = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.STRING)
@JvmStatic val EVOLUTION_STARTED = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
@JvmStatic val TRANSFORMATION_STARTED = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
@JvmStatic val FORM = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.STRING)
@JvmStatic val BUSY_LOCKED = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
@JvmStatic var SHOWN_HELD_ITEM = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.ITEM_STACK)
const val BATTLE_LOCK = "battle"
const val EVOLUTION_LOCK = "evolving"
// Cobblemon busy locks
val SHOULDER_MOUNT_LOCK = cobblemonResource("shoulder_mount")
val CAPTURE = cobblemonResource("capture")
fun createAttributes(): AttributeSupplier.Builder = createLivingAttributes()
.add(Attributes.FOLLOW_RANGE)
@@ -219,14 +222,27 @@ open class PokemonEntity(
/** The player that caused this Pokémon to faint. */
var killer: ServerPlayer? = null
val isEvolving: Boolean
get() = entityData.get(EVOLUTION_STARTED)
val isTransforming: Boolean
get() = entityData.get(TRANSFORMATION_STARTED)
var evolutionEntity: GenericBedrockEntity? = null
var ticksLived = 0
val busyLocks = mutableListOf<Any>()
/**
* Custom locks for marking an entity as busy. When populated, [isBusy] and [BUSY_LOCKED] will always evaluate as TRUE.
*
* NOTE: [BUSY_LOCKED] is NOT a comprehensive check of whether an entity is busy. [isBusy] evaluates Cobblemon-specific
* busy conditions that should ALWAYS be checked.
*/
val busyLocks = mutableListOf<ResourceLocation>()
/**
* Whether this entity is performing a blocking action.
*
* To track an action as blocking, add it to the [busyLocks].
*/
val isBusy: Boolean
get() = busyLocks.isNotEmpty()
get() = entityData.get(BUSY_LOCKED) || isBattling || isTransforming
val aspects: Set<String>
get() = entityData.get(ASPECTS)
var battleId: UUID?
@@ -394,7 +410,9 @@ open class PokemonEntity(
builder.define(FRIENDSHIP, 0)
builder.define(FREEZE_FRAME, -1F)
builder.define(CAUGHT_BALL, "")
builder.define(EVOLUTION_STARTED, false)
builder.define(TRANSFORMATION_STARTED, false)
builder.define(FORM, "")
builder.define(BUSY_LOCKED, false)
builder.define(SHOWN_HELD_ITEM, ItemStack.EMPTY)
}
@@ -407,29 +425,11 @@ open class PokemonEntity(
// common SynchedEntityData handling
when (data) {
SPECIES -> refreshDimensions()
SPECIES, FORM -> refreshDimensions()
POSE_TYPE -> {
val value = entityData.get(data) as PoseType
isNoGravity = (value == PoseType.FLY || value == PoseType.HOVER) && passengers.isEmpty()
}
BATTLE_ID -> {
if (battleId != null) {
busyLocks.remove(BATTLE_LOCK) // Remove in case it's hopped across to another battle, don't want extra battle locks
busyLocks.add(BATTLE_LOCK)
} else {
busyLocks.remove(BATTLE_LOCK)
}
}
EVOLUTION_STARTED -> {
if (isEvolving) {
busyLocks.remove(EVOLUTION_LOCK)
busyLocks.add(EVOLUTION_LOCK)
} else {
busyLocks.remove(EVOLUTION_LOCK)
}
}
}
}
@@ -1157,25 +1157,16 @@ open class PokemonEntity(
return false
}
// Check evolution item interaction
// Check evolution and form change item interaction
if (pokemon.getOwnerPlayer() == player) {
val context = ItemInteractionEvolution.ItemInteractionContext(stack, player.level())
pokemon.lockedEvolutions
.filterIsInstance<ItemInteractionEvolution>()
.forEach { evolution ->
if (evolution.attemptEvolution(pokemon, context)) {
if (!player.isCreative) {
stack.shrink(1)
}
this.level().playSoundServer(
position = this.position(),
sound = CobblemonSounds.ITEM_USE,
volume = 1F,
pitch = 1F
)
return true
}
}
val context = ItemInteractionTrigger.Context(stack, player.level())
val transformed = pokemon.triggerTransformations(ItemInteractionTrigger::class.java, context)
if (transformed.any()) {
if (!player.isCreative) stack.shrink(1)
this.level().playSoundServer(position = this.position(), sound = CobblemonSounds.ITEM_USE, volume = 1F, pitch = 1F)
return true
}
}
(stack.item as? PokemonEntityInteraction)?.let {
@@ -1279,10 +1270,9 @@ open class PokemonEntity(
) {
val dirToPlayer = player.eyePosition.subtract(position()).multiply(1.0, 0.0, 1.0).normalize()
deltaMovement = dirToPlayer.scale(0.8).add(0.0, 0.5, 0.0)
val lock = Any()
busyLocks.add(lock)
busyLocks.add(SHOULDER_MOUNT_LOCK)
after(seconds = 0.5F) {
busyLocks.remove(lock)
busyLocks.remove(SHOULDER_MOUNT_LOCK)
if (!isBusy && isAlive) {
val isLeft = player.shoulderEntityLeft.isEmpty
if (isLeft || player.shoulderEntityRight.isEmpty) {
@@ -1505,7 +1495,7 @@ open class PokemonEntity(
val feature = pokemon.getFeature<FlagSpeciesFeature>(DataKeys.HAS_BEEN_SHEARED)
if (feature != null) {
feature.enabled = false
feature.value = false
pokemon.markFeatureDirty(feature)
pokemon.updateAspects()
}
@@ -1741,7 +1731,7 @@ open class PokemonEntity(
override fun shear(shearedSoundCategory: SoundSource) {
this.level().playSound(null, this, SoundEvents.SHEEP_SHEAR, shearedSoundCategory, 1.0F, 1.0F)
val feature = this.pokemon.getFeature<FlagSpeciesFeature>(DataKeys.HAS_BEEN_SHEARED) ?: return
feature.enabled = true
feature.value = true
this.pokemon.markFeatureDirty(feature)
this.pokemon.updateAspects()
val i = this.random.nextInt(3) + 1
@@ -1776,7 +1766,7 @@ open class PokemonEntity(
override fun readyForShearing(): Boolean {
val feature = this.pokemon.getFeature<FlagSpeciesFeature>(DataKeys.HAS_BEEN_SHEARED) ?: return false
return !this.isBusy && !this.pokemon.isFainted() && !feature.enabled
return !this.isBusy && !this.pokemon.isFainted() && !feature.value
}
override fun canUsePortal(allowsVehicles: Boolean) = false
@@ -142,6 +142,7 @@ class PokemonServerDelegate : PokemonSideDelegate {
val trackedMark = entity.pokemon.activeMark?.identifier.toString()
val trackedAspects = mock?.aspects ?: entity.pokemon.aspects
val trackedBall = mock?.pokeball ?: entity.pokemon.caughtBall.name.toString()
val trackedForm = mock?.form ?: entity.pokemon.form.formOnlyShowdownId()
entity.ownerUUID = entity.pokemon.getOwnerUUID()
entity.entityData.set(PokemonEntity.SPECIES, trackedSpecies)
@@ -163,6 +164,8 @@ class PokemonServerDelegate : PokemonSideDelegate {
entity.entityData.set(PokemonEntity.FRIENDSHIP, entity.pokemon.friendship)
entity.entityData.set(PokemonEntity.CAUGHT_BALL, trackedBall)
entity.entityData.set(PokemonEntity.FORM, trackedForm) // have this get form from aspects...?
entity.entityData.set(PokemonEntity.BUSY_LOCKED, entity.busyLocks.isNotEmpty())
updateShownItem()
updatePoseType()
@@ -212,13 +215,19 @@ class PokemonServerDelegate : PokemonSideDelegate {
}
val battle = getBattle()
if (entity.ticksLived % 20 == 0 && battle != null) {
val activeBattlePokemon = battle
.activePokemon
.find { it.battlePokemon?.uuid == entity.pokemon.uuid }
if (entity.ticksLived % 20 == 0) {
if (battle != null) {
val activeBattlePokemon = battle
.activePokemon
.find { it.battlePokemon?.uuid == entity.pokemon.uuid }
if (activeBattlePokemon != null) {
activeBattlePokemon.position = entity.level() as ServerLevel to entity.position()
if (activeBattlePokemon != null) {
activeBattlePokemon.position = entity.level() as ServerLevel to entity.position()
}
}
// reevaluate forms of wild pokemon
else if (entity.owner == null) {
entity.pokemon.formProxy.server().reevaluate()
}
}
@@ -9,9 +9,10 @@
package com.cobblemon.mod.common.item.interactive
import com.cobblemon.mod.common.api.interaction.PokemonEntityInteraction
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.CobblemonItem
import com.cobblemon.mod.common.pokemon.evolution.variants.TradeEvolution
import com.cobblemon.mod.common.pokemon.transformation.triggers.TradeTrigger
import net.minecraft.world.item.ItemStack
import net.minecraft.server.level.ServerPlayer
@@ -19,20 +20,13 @@ class LinkCableItem : CobblemonItem(Properties()), PokemonEntityInteraction {
override val accepted = setOf(PokemonEntityInteraction.Ownership.OWNER)
override fun processInteraction(player: ServerPlayer, entity: PokemonEntity, stack: ItemStack): Boolean {
val pokemon = entity.pokemon
pokemon.lockedEvolutions.filterIsInstance<TradeEvolution>().forEach { evolution ->
// Prevent Pokémon that require a specific trading partner for evolution (e.g., Shelmet and Karrablast)
// from evolving via link cable. These Pokémon must either be traded with another player or use an
// alternative single-player evolution method.
if (!evolution.requiredContext.originalString.isEmpty()) {
return@forEach
}
// If an evolution is possible non-optional or has been successfully queued we will consume the item and stop
// validate requirements to respect required held items and such.
if (evolution.requirements.all { it.check(pokemon) } && evolution.evolve(pokemon)) {
this.consumeItem(player, stack)
return true
}
// empty properties provided - this will fail for Pokémon that require a specific trading partner
val context = TradeTrigger.Context(PokemonProperties())
val transformed = pokemon.triggerTransformations(TradeTrigger::class.java, context)
if (transformed.any()) {
this.consumeItem(player, stack)
return true
}
return false
}
@@ -11,12 +11,14 @@ 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.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
class FormUpdatePacket(pokemon: () -> Pokemon, form: FormData) : SingleUpdatePacket<FormData, FormUpdatePacket>(pokemon, form) {
override fun encodeValue(buffer: RegistryFriendlyByteBuf) {
this.value.encode(buffer)
buffer.writeString(this.value.name)
}
override fun set(pokemon: Pokemon, value: FormData) {
@@ -29,9 +31,9 @@ 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)
val formName = buffer.readString()
val species = pokemon().species
val form = species.forms.find { it.name == formName } ?: species.standardForm
return FormUpdatePacket(pokemon, form)
}
}
@@ -27,7 +27,7 @@ import net.minecraft.resources.ResourceLocation
* @author Hiroku
* @since November 13th, 2023
*/
class SpeciesFeatureUpdatePacket(pokemon: () -> Pokemon, val species: ResourceLocation, speciesFeature: SynchronizedSpeciesFeature) : SingleUpdatePacket<SynchronizedSpeciesFeature, SpeciesFeatureUpdatePacket>(pokemon, speciesFeature) {
class SpeciesFeatureUpdatePacket(pokemon: () -> Pokemon, val species: ResourceLocation, speciesFeature: SynchronizedSpeciesFeature<*>) : SingleUpdatePacket<SynchronizedSpeciesFeature<*>, SpeciesFeatureUpdatePacket>(pokemon, speciesFeature) {
companion object {
val ID = cobblemonResource("species_feature_update")
fun decode(buffer: RegistryFriendlyByteBuf): SpeciesFeatureUpdatePacket {
@@ -50,7 +50,7 @@ class SpeciesFeatureUpdatePacket(pokemon: () -> Pokemon, val species: ResourceLo
value.saveToBuffer(buffer, toClient = true)
}
override fun set(pokemon: Pokemon, value: SynchronizedSpeciesFeature) {
override fun set(pokemon: Pokemon, value: SynchronizedSpeciesFeature<*>) {
pokemon.features.removeIf { it.name == value.name }
pokemon.features.add(value)
}
@@ -8,24 +8,18 @@
package com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.pokemon.evolution.EvolutionDisplayEvent
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.evolution.EvolutionDisplay
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.cobblemon.mod.common.net.messages.client.pokemon.update.SingleUpdatePacket
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.evolution.CobblemonEvolutionDisplay
import com.cobblemon.mod.common.pokemon.transformation.evolution.EvolutionDisplay
import com.cobblemon.mod.common.util.*
import io.netty.buffer.ByteBuf
import net.minecraft.core.RegistryAccess
import net.minecraft.network.RegistryFriendlyByteBuf
class AddEvolutionPacket(pokemon: () -> Pokemon, value: EvolutionDisplay) : SingleUpdatePacket<EvolutionDisplay, AddEvolutionPacket>(pokemon, value) {
override val id = ID
constructor(pokemon: Pokemon, value: Evolution, registryAccess: RegistryAccess) : this({ pokemon }, value.convertToDisplay(pokemon, registryAccess))
constructor(pokemon: Pokemon, value: Evolution) : this({ pokemon }, value.convertToDisplay(pokemon))
override fun encodeValue(buffer: RegistryFriendlyByteBuf) {
this.value.encode(buffer)
@@ -39,30 +33,6 @@ class AddEvolutionPacket(pokemon: () -> Pokemon, value: EvolutionDisplay) : Sing
val ID = cobblemonResource("add_evolution")
fun decode(buffer: RegistryFriendlyByteBuf) = AddEvolutionPacket(decodePokemon(buffer), decodeDisplay(buffer))
internal fun Evolution.convertToDisplay(pokemon: Pokemon, registryAccess: RegistryAccess): EvolutionDisplay {
val result = pokemon.clone(registryAccess = registryAccess)
this.result.apply(result)
val expectedDisplay = CobblemonEvolutionDisplay(this.id, result)
val event = EvolutionDisplayEvent(result, expectedDisplay, this)
CobblemonEvents.EVOLUTION_DISPLAY.post(event)
return event.display
}
internal fun EvolutionDisplay.encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeString(this.id)
buffer.writeIdentifier(this.species.resourceIdentifier)
buffer.writeCollection(this.aspects) { pb, value -> pb.writeString(value) }
}
internal fun decodeDisplay(buffer: RegistryFriendlyByteBuf): EvolutionDisplay {
val id = buffer.readString()
val speciesIdentifier = buffer.readIdentifier()
val species = PokemonSpecies.getByIdentifier(speciesIdentifier)
?: throw IllegalArgumentException("Cannot resolve species from $speciesIdentifier")
val aspects = buffer.readList(ByteBuf::readString).toSet()
return CobblemonEvolutionDisplay(id, species, aspects)
}
fun decode(buffer: RegistryFriendlyByteBuf) = AddEvolutionPacket(decodePokemon(buffer), EvolutionDisplay.decodeDisplay(buffer))
}
}
@@ -8,22 +8,18 @@
package com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.evolution.EvolutionDisplay
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
import com.cobblemon.mod.common.net.messages.client.pokemon.update.SingleUpdatePacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.AddEvolutionPacket.Companion.convertToDisplay
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.AddEvolutionPacket.Companion.decodeDisplay
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.AddEvolutionPacket.Companion.encode
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.evolution.EvolutionDisplay
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.RegistryAccess
import net.minecraft.network.RegistryFriendlyByteBuf
class RemoveEvolutionPacket(pokemon: () -> Pokemon, value: EvolutionDisplay) : SingleUpdatePacket<EvolutionDisplay, RemoveEvolutionPacket>(pokemon, value) {
override val id = ID
constructor(pokemon: Pokemon, value: Evolution, registryAccess: RegistryAccess) : this({ pokemon }, value.convertToDisplay(pokemon, registryAccess))
constructor(pokemon: Pokemon, value: Evolution) : this({ pokemon }, value.convertToDisplay(pokemon))
override fun encodeValue(buffer: RegistryFriendlyByteBuf) {
this.value.encode(buffer)
@@ -36,7 +32,7 @@ class RemoveEvolutionPacket(pokemon: () -> Pokemon, value: EvolutionDisplay) : S
companion object {
val ID = cobblemonResource("remove_evolution")
fun decode(buffer: RegistryFriendlyByteBuf) = RemoveEvolutionPacket(decodePokemon(buffer), decodeDisplay(buffer))
fun decode(buffer: RegistryFriendlyByteBuf) = RemoveEvolutionPacket(decodePokemon(buffer), EvolutionDisplay.decodeDisplay(buffer))
}
@@ -0,0 +1,30 @@
package com.cobblemon.mod.common.net.messages.client.pokemon.update.form
import com.cobblemon.mod.common.net.messages.client.pokemon.update.SingleUpdatePacket
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.FormDisplay
import com.cobblemon.mod.common.pokemon.transformation.form.TemporaryForm
import com.cobblemon.mod.common.util.*
import net.minecraft.network.RegistryFriendlyByteBuf
class AddFormPacket(pokemon: () -> Pokemon, value: FormDisplay) : SingleUpdatePacket<FormDisplay, AddFormPacket>(pokemon, value) {
override val id = ID
constructor(pokemon: Pokemon, value: TemporaryForm) : this({ pokemon }, value.convertToDisplay(pokemon))
override fun encodeValue(buffer: RegistryFriendlyByteBuf) {
this.value.encode(buffer)
}
override fun set(pokemon: Pokemon, value: FormDisplay) {
pokemon.formProxy.client().add(value)
}
companion object {
val ID = cobblemonResource("add_form")
fun decode(buffer: RegistryFriendlyByteBuf) = AddFormPacket(decodePokemon(buffer), FormDisplay.decode(buffer))
}
}
@@ -0,0 +1,23 @@
package com.cobblemon.mod.common.net.messages.client.pokemon.update.form
import com.cobblemon.mod.common.net.messages.client.PokemonUpdatePacket
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
class ClearFormsPacket(pokemon: () -> Pokemon) : PokemonUpdatePacket<ClearFormsPacket>(pokemon) {
override val id = ID
override fun encodeDetails(buffer: RegistryFriendlyByteBuf) {}
override fun applyToPokemon() {
this.pokemon().formProxy.client().clear()
}
companion object {
val ID = cobblemonResource("clear_forms")
fun decode(buffer: RegistryFriendlyByteBuf) = ClearFormsPacket(decodePokemon(buffer))
}
}
@@ -0,0 +1,30 @@
package com.cobblemon.mod.common.net.messages.client.pokemon.update.form
import com.cobblemon.mod.common.net.messages.client.pokemon.update.SingleUpdatePacket
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.FormDisplay
import com.cobblemon.mod.common.pokemon.transformation.form.TemporaryForm
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
class RemoveFormPacket(pokemon: () -> Pokemon, value: FormDisplay) : SingleUpdatePacket<FormDisplay, RemoveFormPacket>(pokemon, value) {
override val id = ID
constructor(pokemon: Pokemon, value: TemporaryForm) : this({ pokemon }, value.convertToDisplay(pokemon))
override fun encodeValue(buffer: RegistryFriendlyByteBuf) {
this.value.encode(buffer)
}
override fun set(pokemon: Pokemon, value: FormDisplay) {
pokemon.formProxy.client().remove(value)
}
companion object {
val ID = cobblemonResource("remove_form")
fun decode(buffer: RegistryFriendlyByteBuf) = RemoveFormPacket(decodePokemon(buffer), FormDisplay.decode(buffer))
}
}
@@ -31,7 +31,7 @@ class SpawnPokemonPacket(
var speciesId: ResourceLocation,
var gender: Gender,
var shiny: Boolean,
var formName: String,
var formId: String,
var aspects: Set<String>,
var battleId: UUID?,
var phasingTargetId: Int,
@@ -87,7 +87,7 @@ class SpawnPokemonPacket(
buffer.writeIdentifier(this.speciesId)
buffer.writeEnumConstant(this.gender)
buffer.writeBoolean(this.shiny)
buffer.writeString(this.formName)
buffer.writeString(this.formId)
buffer.writeCollection(this.aspects) { pb, value -> pb.writeString(value) }
buffer.writeNullable(this.battleId) { pb, value -> pb.writeUUID(value) }
buffer.writeInt(this.phasingTargetId)
@@ -114,7 +114,7 @@ class SpawnPokemonPacket(
species = this@SpawnPokemonPacket.speciesId.let { PokemonSpecies.getByIdentifier(it) ?: PokemonSpecies.random() }
gender = this@SpawnPokemonPacket.gender
shiny = this@SpawnPokemonPacket.shiny
form = this@SpawnPokemonPacket.formName.let { formName -> species.forms.find { it.formOnlyShowdownId() == formName }} ?: species.standardForm
form = this@SpawnPokemonPacket.formId.let { formId -> species.forms.find { it.formOnlyShowdownId() == formId }} ?: species.standardForm
forcedAspects = this@SpawnPokemonPacket.aspects
nickname = this@SpawnPokemonPacket.nickname
this@SpawnPokemonPacket.mark?.let {activeMark = Marks.getByIdentifier(it) }
@@ -133,7 +133,7 @@ class SpawnPokemonPacket(
entity.entityData.set(PokemonEntity.SPAWN_DIRECTION, spawnYaw)
entity.entityData.set(PokemonEntity.FRIENDSHIP, friendship)
entity.entityData.set(PokemonEntity.FREEZE_FRAME, freezeFrame)
entity.entityData.set(PokemonEntity.FORM, formId)
entity.ejectPassengers()
passengers.forEach {
val passenger = level.getEntity(it) ?: return@forEach
@@ -9,8 +9,8 @@
package com.cobblemon.mod.common.net.messages.server.pokemon.update.evolution
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pokemon.evolution.EvolutionDisplay
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.evolution.EvolutionDisplay
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeString
@@ -0,0 +1,27 @@
package com.cobblemon.mod.common.net.messages.server.pokemon.update.form
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.transformation.form.FormDisplay
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import java.util.*
class AcceptFormPacket(val pokemonUUID: UUID, val formId: String) : NetworkPacket<AcceptFormPacket> {
constructor(pokemon: Pokemon, form: FormDisplay) : this(pokemon.uuid, form.id)
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(this.pokemonUUID)
buffer.writeString(this.formId)
}
companion object {
val ID = cobblemonResource("accept_form")
fun decode(buffer: RegistryFriendlyByteBuf) = AcceptFormPacket(buffer.readUUID(), buffer.readString())
}
}
@@ -0,0 +1,20 @@
package com.cobblemon.mod.common.net.messages.server.pokemon.update.form
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import java.util.*
class RevertFormPacket(val pokemonUUID: UUID) : NetworkPacket<RevertFormPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(this.pokemonUUID)
}
companion object {
val ID = cobblemonResource("revert_form")
fun decode(buffer: RegistryFriendlyByteBuf) = RevertFormPacket(buffer.readUUID())
}
}
@@ -0,0 +1,15 @@
package com.cobblemon.mod.common.net.serverhandling
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.server.pokemon.update.form.AcceptFormPacket
import com.cobblemon.mod.common.util.party
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object AcceptFormHandler : ServerNetworkPacketHandler<AcceptFormPacket> {
override fun handle(packet: AcceptFormPacket, server: MinecraftServer, player: ServerPlayer) {
val pokemon = player.party().get(packet.pokemonUUID) ?: return
val form = pokemon.formProxy.server().getById(packet.formId) ?: return
pokemon.formProxy.server().start(form)
}
}
@@ -0,0 +1,14 @@
package com.cobblemon.mod.common.net.serverhandling
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.server.pokemon.update.form.RevertFormPacket
import com.cobblemon.mod.common.util.party
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object RevertFormHandler : ServerNetworkPacketHandler<RevertFormPacket> {
override fun handle(packet: RevertFormPacket, server: MinecraftServer, player: ServerPlayer) {
val pokemon = player.party().get(packet.pokemonUUID) ?: return
pokemon.formProxy.server().revert()
}
}
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.net.serverhandling.evolution
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.server.pokemon.update.evolution.AcceptEvolutionPacket
import com.cobblemon.mod.common.util.isInBattle
import com.cobblemon.mod.common.util.party
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
@@ -18,8 +17,7 @@ import net.minecraft.server.level.ServerPlayer
object AcceptEvolutionHandler : ServerNetworkPacketHandler<AcceptEvolutionPacket> {
override fun handle(packet: AcceptEvolutionPacket, server: MinecraftServer, player: ServerPlayer) {
val pokemon = player.party()[packet.pokemonUUID] ?: return
if (player.isInBattle() || pokemon.entity?.isBusy == true) return
val evolution = pokemon.evolutionProxy.server().firstOrNull { evolution -> evolution.id.equals(packet.evolutionId, true) } ?: return
val evolution = pokemon.evolutionProxy.server().getById(packet.evolutionId) ?: return
pokemon.evolutionProxy.server().start(evolution)
}
}
@@ -16,18 +16,17 @@ 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.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.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.pokemon.transformation.evolution.PreEvolution
import com.cobblemon.mod.common.api.riding.RidingProperties
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
@@ -36,15 +35,29 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
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.aspects.FORM_ASPECTS
import com.cobblemon.mod.common.pokemon.lighthing.LightingData
import com.cobblemon.mod.common.pokemon.transformation.evolution.Evolution
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.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(
@SerializedName("name")
private var _name: String? = null,
// Internal for the sake of the base stat provider
@SerializedName("baseStats")
internal var _baseStats: MutableMap<Stat, Int>? = null,
@@ -74,16 +87,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,
@@ -99,23 +108,16 @@ class FormData(
private var _weight: Float? = null,
@SerializedName("riding")
private var _riding: RidingProperties? = 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")
private var _lightingData: LightingData? = null
// UPDATE THE SPECIES SCHEMA IF YOU MODIFY THIS! THANKS
) : Decodable, Encodable, ShowdownIdentifiable {
@SerializedName("name")
var name: String = name
private set
/** The name of this form. This is unique among forms belonging to the same [parentForm]. */
val name: String
get() = _name ?: "Standard"
val baseStats: Map<Stat, Int>
get() = _baseStats ?: species.baseStats
@@ -163,11 +165,9 @@ class FormData(
val drops: DropTable
get() = _drops ?: species.drops
/** The aspects applied by this form. For application, see [FORM_ASPECTS]. */
var aspects = mutableListOf<String>()
val preEvolution: PreEvolution?
get() = _preEvolution ?: species.preEvolution
val behaviour = FormPokemonBehaviour()
val dynamaxBlocked: Boolean
@@ -194,12 +194,49 @@ class FormData(
get() = _labels ?: species.labels
/**
* Contains the evolutions of this form.
* The evolution preceding 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()
abstract val preEvolution: PreEvolution?
/**
* 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].
*/
abstract val evolutions: MutableSet<Evolution>
/** The [TemporaryForm]s that are interchangeable with this form. */
abstract val changeableForms: Collection<TemporaryForm>
/**
* The [PermanentForm] 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).
*
* This is initialized during species registration, and is not safe to access until [PokemonSpecies.observable] is emitted.
*/
@delegate:Transient
open val parentForm: PermanentForm? by lazy { parentFormInitializer }
/**
* The base [Species] of this form.
*
* This is initialized during species registration, and is not safe to access until [PokemonSpecies.observable] is emitted.
*/
@delegate:Transient
val species: Species by lazy { speciesInitializer }
@Transient
protected open var parentFormInitializer: PermanentForm? = null
@Transient
private lateinit var speciesInitializer: Species
val battleTheme: ResourceLocation
get() = _battleTheme ?: species.battleTheme
@@ -236,30 +273,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: PermanentForm? = 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()
@@ -302,7 +326,7 @@ class FormData(
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
this.name = buffer.readString()
this._name = buffer.readString()
this.aspects = buffer.readList { buffer.readString() }.toMutableList()
buffer.readNullable { mapBuffer ->
this._baseStats = mapBuffer.readMap(
@@ -339,20 +363,52 @@ class FormData(
}
/**
* The literal Showdown ID of this [formOnlyShowdownId] appended to [Species.showdownId].
* For example Alolan Vulpix becomes 'vulpixalola'
* The literal Showdown ID of this form. This will be a lowercase version of the [name] with all the non-alphanumeric characters removed.
* If this is a nested form, this will be prepended by the formOnlyShowdownId of the [parentForm].
* For example Toxtricity-Low-Key-Gmax becomes 'lowkeygmax' and Vulpix-Alola becomes 'alola'.
*
* @return The literal Showdown ID of this species and form.
* NOTE: this identifier is used for serializing/deserializing the form property of [PokemonProperties],
* and should only be used for uniquely identifying forms of the SAME species.
*
* @return The literal Showdown ID of this form only. If this is the [Species.standardForm], this will be an empty
* string.
*/
fun formOnlyShowdownId(): String = this.parentForm?.let { parent -> parent.formOnlyShowdownId() + ShowdownIdentifiable.REGEX.replace(this.name.lowercase(), "") } ?: ""
// TODO - will including 'standard' cause breakage?
/**
* The complete Showdown ID of this form. This is a concatenation of [Species.showdownId] with [formOnlyShowdownId].
* For example Toxtricity-Low-Key-Gmax becomes 'toxtricitylowkeygmax'
*
* NOTE: this identifier should be used for uniquely identifying forms of DIFFERENT species.
*
* @return The complete Showdown ID of this species and form.
*/
override fun showdownId(): String = this.species.showdownId() + this.formOnlyShowdownId()
/**
* The literal Showdown ID of this form [name].
* This will be a lowercase version of the [name] with all the non-alphanumeric characters removed.
* For example Alolan Vulpix becomes 'alola'
*
* @return The literal Showdown ID of this form only.
*/
fun formOnlyShowdownId(): String = ShowdownIdentifiable.REGEX.replace(this.name.lowercase(), "")
// pokemon.showdownID expects: this.species.showdownID
/**
* The complete Showdown name of this form. This is a concatenation of the appended to [Species.showdownName].
* For example form 'Gmax' of parent form 'Low-Key' of species 'Toxtricity' becomes 'Toxtricity-Low-Key-Gmax'.
*
* NOTE: [StandardForm]s do not exist in Showdown and will instead default to [Species.showdownName].
*
* @return The Showdown name of this species and form.
*/
open fun showdownName(): String = this.parentForm?.let { parent -> parent.showdownName() + "-" + this.name } ?: this.species.showdownName()
// TODO - these only work when retrieved at runtime because setting them on deserializaiton gives defaults... maybe by lazy?
/**
* Checks if two forms are interchangeable with each other.
* This covers the following scenarios:
* 1. This form is a [PermanentForm] and contains the other as a [TemporaryForm]
* 2. Other form is a [PermanentForm] and contains this form as a [TemporaryForm]
* 3. Both forms are [TemporaryForm]s and have the same [parentForm]
*
* @return Whether [other] is interchangeable with this form.
*/
fun isInterchangeableWith(other: FormData) = this.changeableForms.any { it === other } || other.changeableForms.any { it === this }
}

Some files were not shown because too many files have changed in this diff Show More