Compare commits

...
Author SHA1 Message Date
Apion 0dd0a78741 Cleaned up DamageInstruction 2025-03-23 15:05:31 -04:00
Hiroku 03ddc35d36 Merge branch 'Fix-pokemon-loadFrom' into 'main'
Fixed Pokemon.loadFrom[JSON/NBT]

Closes #1507

See merge request cable-mc/cobblemon!1571
2025-03-23 03:22:17 +00:00
ShiroFenixandHiroku 1b8e9b9b36 Fixed Pokemon.loadFrom[JSON/NBT] 2025-03-23 03:22:17 +00:00
Paul e52ac956f0 Added q.entity.is_holding_item for pokemon posers to use 2025-03-23 12:40:59 +10:00
Hiroku 493664bef2 Merge branch 'mealam/pokeballlocation' into 'main'
fix: Adjust `NPCEntity` pokeball throw positioning to account for `baseScale`

See merge request cable-mc/cobblemon!1568
2025-03-21 23:50:33 +00:00
AramandHiroku 981e3e5efc fix: Adjust NPCEntity pokeball throw positioning to account for baseScale 2025-03-21 23:50:33 +00:00
Hiroku c15350e647 Merge branch 'mealam/1499' into 'main'
fix: Adjust `NPCEntity` beam positioning to account for `baseScale`

Closes #1499

See merge request cable-mc/cobblemon!1567
2025-03-20 00:36:12 +00:00
AramandHiroku f9e5784b10 fix: Adjust NPCEntity beam positioning to account for baseScale 2025-03-20 00:36:12 +00:00
5 changed files with 136 additions and 96 deletions
+3
View File
@@ -58,6 +58,9 @@
- Note: This will break mods that used our observable functionality there or in MoveSet, IVs, EVs, or BenchedMoves.
- Using `Pokemon#onChange()` is now the way to mark a Pokémon as needing a save.
- Using `[Pokemon].changeObservable` is now the way to get an `Observable` for any save-worthy changes.
- Updated NPCEntity beam positioning to properly account for the baseScale property.
- Updated NPCEntity pokeball throw positioning to properly account for the baseScale property.
- Fixed `[Pokemon].copyFrom` error causing forms, IVs, and EVs to not be applied properly when using `[Pokemon].loadFromJSON` or `[Pokemon].loadFromNBT`
### MoLang & Datapacks
- The following usages for item predicates can now use item conditions like advancements do, you can learn about them in the [Minecraft wiki](https://minecraft.wiki/w/Advancement_definition#minecraft:filled_bucket)
@@ -788,6 +788,7 @@ object MoLangFunctions {
map.put("is_flying") { DoubleValue(pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING)) }
map.put("has_aspect") { DoubleValue(it.getString(0) in pokemonEntity.aspects) }
map.put("is_pokemon") { DoubleValue.ONE }
map.put("is_holding_item") { DoubleValue(!pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).isEmpty) }
map
}
)
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.battles.interpreter.instructions
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.interpreter.Effect
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
@@ -23,11 +24,13 @@ import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
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.PoisonBadlyStatus
import com.cobblemon.mod.common.pokemon.status.statuses.persistent.PoisonStatus
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.battleLang
@@ -67,49 +70,40 @@ class DamageInstruction(
}
}
}
val newHealth = privateMessage.argumentAt(1)?.split(" ")?.get(0) ?: return
val effect = privateMessage.effect()
val source = privateMessage.battlePokemonFromOptional(battle)
source?.let { ShowdownInterpreter.broadcastOptionalAbility(battle, effect, it) }
}
private fun doRecoilEvoChecks(battlePokemon: BattlePokemon) {
battlePokemon.effectedPokemon.let { pokemon ->
if (RecoilEvolutionProgress.supports(pokemon)) {
val healthStr = privateMessage.argumentAt(1) ?: throw UnsupportedOperationException(
"Cant get recoil string"
)
val newHealth =
"([0-9]+).*".toRegex().find(healthStr)?.groups?.get(1)?.value?.toIntOrNull()
?: throw UnsupportedOperationException("Cant get recoil string")
val difference = pokemon.currentHealth - newHealth
if (difference > 0) {
val progress = pokemon.evolutionProxy.current()
.progressFirstOrCreate({ it is RecoilEvolutionProgress }) { RecoilEvolutionProgress() }
progress.updateProgress(RecoilEvolutionProgress.Progress(progress.currentProgress().recoil + difference))
}
}
if (source != null) {
ShowdownInterpreter.broadcastOptionalAbility(battle, effect, source)
}
}
override fun runActionEffect(battle: PokemonBattle, runtime: MoLangRuntime) {
val effect = privateMessage.effect()
val battlePokemon = publicMessage.battlePokemon(0, actor.battle) ?: return
val publicPokemon = publicMessage.battlePokemon(0, actor.battle) ?: return
var status = effect?.id?.let { Statuses.getStatus(it) }
battle.dispatch {
val pokemon = privateMessage.battlePokemon(0, battle) ?: return@dispatch GO
//Showdown doesnt tell us on damage if its poison or toxic so we gotta consult the entity
if (status is PoisonStatus) {
status = pokemon.effectedPokemon.status?.status ?: status
val isToxic = publicPokemon.effectedPokemon.status?.status is PoisonBadlyStatus
val realStatus = if (isToxic) PoisonBadlyStatus() else status
// Damage effects without a action effect receive the generic damage effect
val actionEffect = if (realStatus != null) {
realStatus.getActionEffect()
} else if (effect != null) {
ActionEffects.actionEffects[cobblemonResource("generic_damage")]
} else {
null
}
// Damage effects without a action effect receive the generic damage effect
val actionEffect = status?.getActionEffect() ?: effect?.let {
ActionEffects.actionEffects["generic_damage".asIdentifierDefaultingNamespace()]
} ?: return@dispatch GO
if (actionEffect == null) {
return@dispatch GO
}
val providers = mutableListOf<Any>(battle)
battlePokemon.effectedPokemon.entity?.let { UsersProvider(it) }?.let(providers::add)
val pokemonEntity = publicPokemon.effectedPokemon.entity
if (pokemonEntity != null) {
providers.add(UsersProvider(pokemonEntity))
}
val context = ActionEffectContext(
actionEffect = actionEffect,
@@ -125,72 +119,24 @@ class DamageInstruction(
}
override fun postActionEffect(battle: PokemonBattle) {
val newHealth = privateMessage.argumentAt(1)?.split(" ")?.get(0) ?: return
val hpStatus = privateMessage.argumentAt(1)?.split(" ")?.get(0)!!
var causedFaint = hpStatus == "0"
val remainingHealth = hpStatus.split("/")[0].toFloat()
val maxHealth = hpStatus.split("/").getOrNull(1)?.toFloat()
val battlePokemon = publicMessage.battlePokemon(0, actor.battle) ?: return
var causedFaint = newHealth == "0"
val effect = privateMessage.effect()
val source = privateMessage.battlePokemonFromOptional(battle)
val lastCauser = instructionSet.getMostRecentCauser(comparedTo = this)
val pokemonName = battlePokemon.getName()
val pokemonEntity = battlePokemon.entity
battle.dispatch {
val pokemonName = battlePokemon.getName()
val pokemonEntity = battlePokemon.entity
//Play recoil animation if the pokemon recoiling isnt dead
if (!causedFaint && pokemonEntity != null) {
val pkt = PlayPosableAnimationPacket(pokemonEntity.id, setOf("recoil"), emptyList())
pkt.sendToPlayersAround(
x = pokemonEntity.x,
y = pokemonEntity.y,
z = pokemonEntity.z,
worldKey = pokemonEntity.level().dimension(),
distance = 50.0
)
}
val newHealthRatio: Float
val remainingHealth = newHealth.split("/")[0].toInt()
playRecoilAnim(pokemonEntity, causedFaint)
if (effect != null) {
val lang = when (effect.id) {
"blacksludge", "stickybarb", "jabocaberry", "rowapberry" -> battleLang("damage.item", pokemonName, effect.typelessData)
"brn", "psn", "tox" -> {
val status = Statuses.getStatus(effect.id)?.name?.path ?: return@dispatch GO
lang("status.$status.hurt", pokemonName)
}
"aftermath", "innardsout" -> battleLang("damage.generic", pokemonName)
"chloroblast", "steelbeam" -> battleLang("damage.mindblown", pokemonName)
"jumpkick" -> battleLang("damage.highjumpkick", pokemonName)
else -> battleLang("damage.${effect.id}", pokemonName, source?.getName() ?: Component.literal("UNKOWN"))
}
battle.broadcastChatMessage(lang.red())
sendStatusMessage(battle, effect, pokemonName, source)
}
if (causedFaint) {
newHealthRatio = 0F
battle.dispatch {
battlePokemon.effectedPokemon.currentHealth = 0
battlePokemon.sendUpdate()
GO
}
causedFaint = true
} else {
val maxHealth = newHealth.split("/")[1].toInt()
val difference = maxHealth - remainingHealth
newHealthRatio = remainingHealth.toFloat() / maxHealth
battle.dispatchToFront {
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))
}
}
}
battlePokemon.sendUpdate()
GO
}
}
privateMessage.pnxAndUuid(0)?.let { (pnx, _) -> battle.sendSidedUpdate(actor, BattleHealthChangePacket(pnx, remainingHealth.toFloat()), BattleHealthChangePacket(pnx, newHealthRatio)) }
updateHealth(battle, battlePokemon, remainingHealth, maxHealth)
battle.minorBattleActions[battlePokemon.uuid] = privateMessage
if (lastCauser is MoveInstruction) {
@@ -215,4 +161,82 @@ class DamageInstruction(
}
}
private fun updateHealth(battle: PokemonBattle, battlePokemon: BattlePokemon, remainingHealth: Float, maxHealth: Float?) {
val causedFaint = maxHealth == null
val newHealthRatio = if (maxHealth == null) 0F else remainingHealth / maxHealth
if (causedFaint) {
battle.dispatch {
battlePokemon.effectedPokemon.currentHealth = 0
battlePokemon.sendUpdate()
GO
}
}
else {
val difference = maxHealth - remainingHealth
battle.dispatchToFront {
battlePokemon.effectedPokemon.currentHealth = remainingHealth.toInt()
updateDamageTakenEvo(battlePokemon, difference.toInt())
battlePokemon.sendUpdate()
GO
}
}
privateMessage.pnxAndUuid(0)?.let { (pnx, _ ) ->
battle.sendSidedUpdate(actor, BattleHealthChangePacket(pnx, remainingHealth), BattleHealthChangePacket(pnx, newHealthRatio))
}
}
private fun doRecoilEvoChecks(battlePokemon: BattlePokemon) {
battlePokemon.effectedPokemon.let { pokemon ->
if (RecoilEvolutionProgress.supports(pokemon)) {
val healthStr = privateMessage.argumentAt(1) ?: throw UnsupportedOperationException("Cant get recoil string")
val newHealth =
"([0-9]+).*".toRegex().find(healthStr)?.groups?.get(1)?.value?.toIntOrNull()
?: throw UnsupportedOperationException("Cant get recoil string")
val difference = pokemon.currentHealth - newHealth
if (difference > 0) {
val progress = pokemon.evolutionProxy.current()
.progressFirstOrCreate({ it is RecoilEvolutionProgress }) { RecoilEvolutionProgress() }
progress.updateProgress(RecoilEvolutionProgress.Progress(progress.currentProgress().recoil + difference))
}
}
}
}
private fun sendStatusMessage(battle: PokemonBattle, effect: Effect, pokemonName: Component, source: BattlePokemon?) {
val lang = when (effect.id) {
"blacksludge", "stickybarb", "jabocaberry", "rowapberry" -> battleLang("damage.item", pokemonName, effect.typelessData)
"brn", "psn", "tox" -> {
val status = Statuses.getStatus(effect.id)?.name?.path
lang("status.$status.hurt", pokemonName)
}
"aftermath", "innardsout" -> battleLang("damage.generic", pokemonName)
"chloroblast", "steelbeam" -> battleLang("damage.mindblown", pokemonName)
"jumpkick" -> battleLang("damage.highjumpkick", pokemonName)
else -> battleLang("damage.${effect.id}", pokemonName, source?.getName() ?: Component.literal("UNKOWN"))
}
battle.broadcastChatMessage(lang.red())
}
private fun playRecoilAnim(pokemonEntity: PokemonEntity?, causedFaint: Boolean) {
if (!causedFaint && pokemonEntity != null) {
val pkt = PlayPosableAnimationPacket(pokemonEntity.id, setOf("recoil"), emptyList())
pkt.sendToPlayersAround(
x = pokemonEntity.x,
y = pokemonEntity.y,
z = pokemonEntity.z,
worldKey = pokemonEntity.level().dimension(),
distance = 50.0
)
}
}
private fun updateDamageTakenEvo(battlePokemon: BattlePokemon, damageTaken: Int) {
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 + damageTaken))
}
}
}
}
@@ -222,8 +222,14 @@ class PokemonRenderer(
val phaseTarget = clientDelegate.phaseTarget ?: return
poseMatrix.pushPose()
var beamSourcePosition = if (phaseTarget is PosableEntity) {
(phaseTarget.delegate as PosableState).locatorStates["beam"]?.getOrigin() ?: phaseTarget.position()
var beamSourcePosition = if (phaseTarget is NPCEntity) {
val npcDelegate = phaseTarget.delegate as NPCClientDelegate
val baseScale = phaseTarget.baseScale?.toDouble() ?: 1.0
(npcDelegate.locatorStates["beam"]?.getOrigin()?.scale(baseScale))
?: phaseTarget.position().add(0.0, (phaseTarget.bbHeight / 2.0) * baseScale, 0.0)
} else if (phaseTarget is PosableEntity) {
(phaseTarget.delegate as PosableState).locatorStates["beam"]?.getOrigin() ?: phaseTarget.position()
} else {
if (phaseTarget.uuid == Minecraft.getInstance().player?.uuid) {
val lookVec = phaseTarget.lookAngle.yRot((PI / 2).toFloat()).multiply(1.0, 0.0, 1.0).normalize()
@@ -303,7 +309,11 @@ class PokemonRenderer(
val lookVec = beamTarget.lookAngle.yRot((PI / 2).toFloat()).multiply(1.0, 0.0, 1.0).normalize()
beamTarget.getEyePosition(partialTicks).subtract(0.0, 0.4, 0.0).subtract(lookVec.scale(0.3))
} else if (beamTarget is NPCEntity) {
(beamTarget.delegate as NPCClientDelegate).locatorStates["beam"]?.getOrigin() ?: beamTarget.position()
val npcDelegate = beamTarget.delegate as NPCClientDelegate
val baseScale = beamTarget.baseScale?.toDouble() ?: 1.0
(npcDelegate.locatorStates["beam"]?.getOrigin()?.scale(baseScale))
?: beamTarget.position().add(0.0, (beamTarget.bbHeight / 2.0) * baseScale, 0.0)
} else {
val lookVec = beamTarget.lookAngle.yRot((PI / 2 - (beamTarget.visualRotationYInDegrees - beamTarget.xRot).toRadians()).toFloat()).multiply(1.0, 0.0, 1.0).normalize()
beamTarget.getEyePosition(partialTicks).subtract(0.0, 0.7, 0.0).subtract(lookVec.scale(0.4))
@@ -1069,14 +1069,14 @@ open class Pokemon : ShowdownIdentifiable {
this.experience = other.experience
this.setFriendship(other.friendship)
// Applied before current health for calcs to take place
other.ivs.doWithoutEmitting {
this.ivs.forEach {
other.ivs[it.key] = it.value
this.ivs.doWithoutEmitting {
other.ivs.forEach {
this.ivs[it.key] = it.value
}
}
other.evs.doWithoutEmitting {
this.evs.forEach {
other.evs[it.key] = it.value
this.evs.doWithoutEmitting {
other.evs.forEach {
this.evs[it.key] = it.value
}
}
this.currentHealth = other.currentHealth
@@ -1105,7 +1105,9 @@ open class Pokemon : ShowdownIdentifiable {
this.originalTrainerType = other.originalTrainerType
this.originalTrainer = other.originalTrainer
this.forcedAspects = other.forcedAspects
this.features = other.features
this.cosmeticItem = other.cosmeticItem
this.updateAspects()
this.refreshOriginalTrainer()
this.initialize()
return this