Merge branch 'feat/capture-qol-feature-parity' into 'main'

Critical capture feature parity

See merge request cable-mc/cobblemon!1516
This commit is contained in:
Polymeta
2025-02-14 16:35:16 +00:00
7 changed files with 111 additions and 32 deletions
+2
View File
@@ -32,6 +32,8 @@
- Renamed Cobblemon's creative tabs to start with "Cobblemon: " to distinguish Cobblemon's tabs from tabs for other mods.
- Various items now have a rarity value.
### Changes
- When using the `cobblemon` or `generation_9` capture calculators a critical capture with a single shake will always play for successful captures when you've already registered the Pokémon as caught in your Pokédex.
### 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)
- The `requiredContext` for an item interaction evolution
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.api.events.pokemon
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.api.pokemon.stats.EvSource
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.pokemon.Pokemon
/**
* Fired during EV mutation.
@@ -25,6 +26,11 @@ interface EvGainedEvent {
*/
val stat: Stat
/**
* @see EvSource.pokemon
*/
val pokemon: Pokemon get() = this.source.pokemon
/**
* The [EvSource] that fired this event.
*/
@@ -0,0 +1,53 @@
/*
* 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.pokeball.catching.calculators
import com.cobblemon.mod.common.api.pokeball.catching.CaptureContext
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.pokedex
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.LivingEntity
/**
* An influencer on capture results based on Pokedex data.
*/
interface PokedexStatusCaptureInfluencer {
/**
* Attempts to influence the [context] of a capture.
*
* @param thrower The [LivingEntity] that threw the Pokeball
* @param target The [PokemonEntity] attempting to capture.
* @param context The calculated [CaptureContext].
* @return The final [CaptureContext].
*/
fun influence(thrower: LivingEntity, target: PokemonEntity, context: CaptureContext): CaptureContext {
if (!context.isSuccessfulCapture) {
return context
}
val player = thrower as? ServerPlayer ?: return context
val pokedex = player.pokedex()
val speciesId = target.pokemon.species.resourceIdentifier
val formName = target.pokemon.form.name
val hasCaughtStatus = pokedex.getOrCreateSpeciesRecord(speciesId)
.getOrCreateFormRecord(formName)
.knowledge == PokedexEntryProgress.CAUGHT
// In modern games when the catch is a success and the Pokémon has already been registered to the Pokédex
// the catch becomes a critical capture and only requires one shake
return if (hasCaughtStatus) {
CaptureContext(
numberOfShakes = 1,
isSuccessfulCapture = true,
isCriticalCapture = true
)
} else context
}
}
@@ -21,8 +21,9 @@ import kotlin.random.Random
abstract class PokeBallPosableState : PosableState(), Schedulable {
abstract val stateEmitter: SettableObservable<EmptyPokeBallEntity.CaptureState>
abstract val shakeEmitter: Observable<Unit>
private val group: String
protected open val group: String
get() = if (this.currentModel is AncientPokeBallModel) "ancient_poke_ball" else "poke_ball"
protected open var lastKnownState: EmptyPokeBallEntity.CaptureState? = null
open fun initSubscriptions() {
stateEmitter.subscribe { state ->
@@ -44,9 +45,17 @@ abstract class PokeBallPosableState : PosableState(), Schedulable {
}
EmptyPokeBallEntity.CaptureState.FALL -> {}
EmptyPokeBallEntity.CaptureState.SHAKE -> {
EmptyPokeBallEntity.CaptureState.CRITICAL -> {
doLater {
setActiveAnimations(currentModel!!.bedrockStateful(group, "bounce"))
setActiveAnimations(currentModel!!.bedrockStateful(group, "critical"))
}
}
EmptyPokeBallEntity.CaptureState.SHAKE -> {
if (this.lastKnownState != EmptyPokeBallEntity.CaptureState.CRITICAL) {
// Skip the bounce on a critical capture, the animation replaces this step
doLater {
setActiveAnimations(currentModel!!.bedrockStateful(group, "bounce"))
}
}
shakeEmitter
.pipe(Observable.emitWhile { stateEmitter.get() == EmptyPokeBallEntity.CaptureState.SHAKE })
@@ -60,11 +69,6 @@ abstract class PokeBallPosableState : PosableState(), Schedulable {
setActiveAnimations(currentModel!!.bedrockStateful(group, "capture"))
}
}
EmptyPokeBallEntity.CaptureState.CAPTURED_CRITICAL -> {
doLater {
setActiveAnimations(currentModel!!.bedrockStateful(group, "critical"))
}
}
EmptyPokeBallEntity.CaptureState.BROKEN_FREE -> {
doLater {
setActiveAnimations(currentModel!!.bedrockStateful(group, "break"))
@@ -72,6 +76,7 @@ abstract class PokeBallPosableState : PosableState(), Schedulable {
}
else -> {}
}
this.lastKnownState = state
}
}
}
@@ -72,16 +72,15 @@ import net.minecraft.world.phys.EntityHitResult
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import java.util.concurrent.CompletableFuture
import kotlin.compareTo
class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragModifier, Schedulable {
enum class CaptureState {
NOT,
HIT,
FALL,
CRITICAL,
SHAKE,
CAPTURED,
CAPTURED_CRITICAL,
BROKEN_FREE
}
@@ -138,14 +137,13 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
delegate.onSyncedDataUpdated(data)
}
if (data == CAPTURE_STATE) {
val newState = entityData.get(CAPTURE_STATE)
val newState = entityData[CAPTURE_STATE]
when (CaptureState.entries[newState.toInt()]) {
CaptureState.NOT -> setNoGravity(false)
CaptureState.HIT -> {}
CaptureState.FALL -> setNoGravity(false)
CaptureState.SHAKE -> setNoGravity(true)
CaptureState.CAPTURED, CaptureState.CAPTURED_CRITICAL -> {}
CaptureState.BROKEN_FREE -> {}
CaptureState.NOT, CaptureState.FALL -> isNoGravity = false
CaptureState.CRITICAL, CaptureState.SHAKE -> isNoGravity = true
CaptureState.HIT, CaptureState.CAPTURED, CaptureState.BROKEN_FREE -> {
// no op
}
}
}
dataTrackerEmitter.emit(data)
@@ -339,14 +337,14 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
if (rollsRemaining <= 0) {
if (captureResult.isSuccessfulCapture) {
captureState = if (captureResult.isCriticalCapture) CaptureState.CAPTURED_CRITICAL else CaptureState.CAPTURED
captureState = CaptureState.CAPTURED
// Do a capture
val pokemon = capturingPokemon ?: return
val player = this.owner as? ServerPlayer ?: return
val captureTime = if (pokeBall.ancient == true) 1.8F else 1F
after(seconds = captureTime) {
// Dupes occurred by double-adding Pokémon, this hopefully prevents it triple-condom style
// wtf did I just read above me
if (pokemon.pokemon.isWild() && pokemon.isAlive && !captureFuture.isDone) {
pokemon.discard()
discard()
@@ -406,7 +404,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
pokemon.busyLocks.remove(this)
captureFuture.complete(false)
val ballType =
this.pokeBall.name.path.toString().toLowerCase().replace("_", "")
this.pokeBall.name.path.lowercase().replace("_", "")
val mode = "casual"
val sendflash =
BedrockParticleOptionsRepository.getEffect(cobblemonResource("${ballType}/${mode}/sendflash"))
@@ -473,7 +471,6 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
override fun onHit(hitResult: HitResult) {
super.onHit(hitResult)
if (captureState == CaptureState.FALL && hitResult.type == HitResult.Type.BLOCK) {
captureState = CaptureState.SHAKE
if (level().isServerSide()) {
beginCapture()
}
@@ -494,12 +491,26 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
if (rollsRemaining == 4) {
rollsRemaining--
}
var animatedCritical = false
// Critical capture despite their name actually animate first
// They can still fail and don't actually signify a success
val iterations: Int = if (captureResult.isCriticalCapture) {
captureState = CaptureState.CRITICAL
captureResult.numberOfShakes + 2
} else {
captureState = CaptureState.SHAKE
captureResult.numberOfShakes + 1
}
taskBuilder()
.iterations(captureResult.numberOfShakes + 1)
.iterations(iterations)
.delay(SECONDS_BEFORE_SHAKE)
.interval(SECONDS_BETWEEN_SHAKES)
.execute {
if (captureResult.isCriticalCapture && !animatedCritical) {
captureState = CaptureState.SHAKE
animatedCritical = true
return@execute
}
if (pokeBall.ancient && rollsRemaining > 1) {
rollsRemaining = 1
}
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.pokeball.catching.CaptureContext
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CriticalCaptureProvider
import com.cobblemon.mod.common.api.pokeball.catching.calculators.PokedexProgressCaptureMultiplierProvider
import com.cobblemon.mod.common.api.pokeball.catching.calculators.PokedexStatusCaptureInfluencer
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
@@ -32,7 +33,7 @@ import kotlin.random.Random
import net.minecraft.world.entity.LivingEntity
import net.minecraft.server.level.ServerPlayer
object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, PokedexProgressCaptureMultiplierProvider {
object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, PokedexProgressCaptureMultiplierProvider, PokedexStatusCaptureInfluencer {
override fun id(): String = "cobblemon"
@@ -54,9 +55,9 @@ object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, P
val pokeBall = pokeBallEntity.pokeBall
val pokemon = target.pokemon
if (pokeBall.catchRateModifier.isGuaranteed()) {
return CaptureContext.successful()
return this.influence(thrower, target, CaptureContext.successful())
}
// We don't have dark grass so we're just gonna pretend everything is that. Scratch that, without the pokedex it has issues.
// We don't have dark grass so we're just going to pretend everything is that. Scratch that, without the Pokédex it has issues.
val darkGrass = 1F //if (thrower is ServerPlayer) this.caughtMultiplierFor(thrower).roundToInt() else 1
val inBattleModifier = if (target.battleId != null) 1F else 0.5F
val catchRate = getCatchRate(thrower, pokeBallEntity, target, pokemon.form.catchRate.toFloat())
@@ -94,8 +95,8 @@ object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, P
return CaptureContext(numberOfShakes = 1, isSuccessfulCapture = shakes == 1, isCriticalCapture = true)
}
}
// ToDo once pokedex is implemented if the target is registered and it's a success shorten to 1 shake and become critical capture
return CaptureContext(numberOfShakes = shakes, isSuccessfulCapture = shakes == 4, isCriticalCapture = false)
return this.influence(thrower, target, CaptureContext(numberOfShakes = shakes, isSuccessfulCapture = shakes == 4, isCriticalCapture = false))
}
private fun findHighestThrowerLevel(player: ServerPlayer, pokemon: Pokemon): Int? {
@@ -109,4 +110,5 @@ object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, P
} ?: return null
return actor.getSide().getOppositeSide().activePokemon.maxOfOrNull { it.battlePokemon?.effectedPokemon?.level ?: 1 }
}
}
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.pokeball.catching.CaptureContext
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CriticalCaptureProvider
import com.cobblemon.mod.common.api.pokeball.catching.calculators.PokedexProgressCaptureMultiplierProvider
import com.cobblemon.mod.common.api.pokeball.catching.calculators.PokedexStatusCaptureInfluencer
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.status.statuses.persistent.BurnStatus
@@ -34,7 +35,7 @@ import net.minecraft.server.level.ServerPlayer
* @author Licious
* @since January 29th, 2022
*/
object Gen9CaptureCalculator : CaptureCalculator, CriticalCaptureProvider, PokedexProgressCaptureMultiplierProvider {
object Gen9CaptureCalculator : CaptureCalculator, CriticalCaptureProvider, PokedexProgressCaptureMultiplierProvider, PokedexStatusCaptureInfluencer {
override fun id(): String = "generation_9"
@@ -42,7 +43,7 @@ object Gen9CaptureCalculator : CaptureCalculator, CriticalCaptureProvider, Poked
val pokeBall = pokeBallEntity.pokeBall
val pokemon = target.pokemon
if (pokeBall.catchRateModifier.isGuaranteed()) {
return CaptureContext.successful()
return this.influence(thrower, target, CaptureContext.successful())
}
// We don't have dark grass so we're just gonna pretend everything is that.
val darkGrass = if (thrower is ServerPlayer) this.caughtMultiplierFor(thrower).roundToInt() else 1
@@ -70,8 +71,7 @@ object Gen9CaptureCalculator : CaptureCalculator, CriticalCaptureProvider, Poked
return CaptureContext(numberOfShakes = 1, isSuccessfulCapture = shakes == 1, isCriticalCapture = true)
}
}
// ToDo once pokedex is implemented if the target is registered and it's a success shorten to 1 shake and become critical capture
return CaptureContext(numberOfShakes = shakes, isSuccessfulCapture = shakes == 4, isCriticalCapture = false)
return this.influence(thrower, target, CaptureContext(numberOfShakes = shakes, isSuccessfulCapture = shakes == 4, isCriticalCapture = false))
}
}