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
8 changed files with 136 additions and 214 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)
@@ -62,7 +62,6 @@ object CobblemonCommands {
RunMolangScriptCommand.register(dispatcher)
CobblemonConfigCommand.register(dispatcher)
UnlockPCBoxWallpaperCommand.register(dispatcher)
AlterShinyChanceCommand.register(dispatcher)
// Possibly lock down registration if and only if under dev environment or running in an environment
// with a certain system environment variable set
@@ -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
}
)
@@ -13,9 +13,6 @@ object CobblemonPermissions {
private const val COMMAND_PREFIX = "command."
private val permissions = arrayListOf<Permission>()
@JvmStatic
val ALTER_SHINY_CHANCE = this.create("${COMMAND_PREFIX}altershinychance", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val CHANGE_EYE_HEIGHT = this.create("${COMMAND_PREFIX}changeeyeheight", PermissionLevel.ALL_COMMANDS)
@JvmStatic
@@ -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))
@@ -1,114 +0,0 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.util.permission
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.context.CommandContext
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.server.level.ServerPlayer
import net.minecraft.network.chat.Component
import net.minecraft.server.MinecraftServer
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import java.util.LinkedList
object AlterShinyChanceCommand {
private var currentTask: Runnable? = null
private var remainingTime: Long = 0
private var shinyRate: Float = config.shinyRate
private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
private val queue: LinkedList<Pair<Float, Long>> = LinkedList()
fun register(dispatcher: CommandDispatcher<CommandSourceStack>) {
dispatcher.register(
Commands.literal("altershinychance")
.permission(CobblemonPermissions.ALTER_SHINY_CHANCE)
.then(
Commands.argument("rate", IntegerArgumentType.integer(1))
.executes { execute(it, it.source.server, it.source.playerOrException, null) }
.then(
Commands.argument("duration", IntegerArgumentType.integer(1))
.requires { it.player != null }
.executes { execute(it, it.source.server, it.source.playerOrException, IntegerArgumentType.getInteger(it, "duration").toLong()) }
)
)
.then(
Commands.literal("skip")
.requires { it.player != null }
.executes { skipNext(it, it.source.server, it.source.playerOrException) }
)
.then(
Commands.literal("clear")
.requires { it.player != null }
.executes { clearQueue(it, it.source.server, it.source.playerOrException) }
)
)
}
private fun execute(context: CommandContext<CommandSourceStack>, server: MinecraftServer, player: ServerPlayer, duration: Long?): Int {
val rate = IntegerArgumentType.getInteger(context, "rate").toFloat()
if (duration == null) {
// Permanent change
config.shinyRate = rate
queue.clear()
player.sendSystemMessage(Component.literal("Shiny rate permanently set to 1 in ${rate.toInt()}"))
} else {
queue.add(Pair(rate, duration))
if (currentTask == null) {
startNextShinyRate(server, player)
} else {
player.sendSystemMessage(Component.literal("Shiny rate queued to 1 in ${rate.toInt()} for $duration seconds."))
}
}
return Command.SINGLE_SUCCESS
}
private fun startNextShinyRate(server: MinecraftServer, player: ServerPlayer) {
if (queue.isEmpty()) {
config.shinyRate = shinyRate // Reset to original
currentTask = null
player.sendSystemMessage(Component.literal("Shiny rate has returned to 1 in ${shinyRate.toInt()}"))
return
}
val (rate, duration) = queue.poll()
config.shinyRate = rate
remainingTime = duration
player.sendSystemMessage(Component.literal("Shiny rate set to 1 in ${rate.toInt()} for $duration seconds."))
currentTask = Runnable {
if (remainingTime > 0) {
remainingTime -= 10
scheduler.schedule(currentTask, 10, TimeUnit.SECONDS)
} else {
startNextShinyRate(server, player)
}
}
scheduler.schedule(currentTask, 10, TimeUnit.SECONDS)
}
private fun skipNext(context: CommandContext<CommandSourceStack>, server: MinecraftServer, player: ServerPlayer): Int {
if (queue.isNotEmpty()) {
player.sendSystemMessage(Component.literal("Skipping current shiny rate duration."))
startNextShinyRate(server, player)
} else {
player.sendSystemMessage(Component.literal("No active shiny rate durations to skip."))
}
return Command.SINGLE_SUCCESS
}
private fun clearQueue(context: CommandContext<CommandSourceStack>, server: MinecraftServer, player: ServerPlayer): Int {
queue.clear()
remainingTime = 0
config.shinyRate = shinyRate
currentTask = null
player.sendSystemMessage(Component.literal("All shiny rate durations cleared. Shiny rate reset to 1 in ${shinyRate.toInt()}"))
return Command.SINGLE_SUCCESS
}
}
@@ -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