Merge branch 'move-animations' into 'main'

Simplistic move animations

See merge request cable-mc/cobblemon!871
This commit is contained in:
Nick
2024-02-10 22:51:29 +00:00
730 changed files with 10593 additions and 2168 deletions
+1
View File
@@ -33,3 +33,4 @@ common/src/main/resources/assets/cobblemon/showdown
# Generated files
*/generated/resources/*
/fabric/src/generated/resources/cobblemon-common.accesswidener
/.ignored/
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 B

+1
View File
@@ -19,6 +19,7 @@
- You can now find Nether Fire Stone Ore in, surprisingly, the Nether.
- Added effects for many Poké Balls when sending out Pokémon or capturing.
- Added a new send-out ball toss animation that showcases the ball used for the Pokémon.
- Added simple move animations for Pokémon in battle with a proof of concept for more complicated animations.
### Changes
- Pokémon now transition more smoothly between different poses.
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.client.net.PlayerInteractOptionsHandler
import com.cobblemon.mod.common.client.net.SetClientPlayerDataHandler
import com.cobblemon.mod.common.client.net.animation.PlayPoseableAnimationHandler
import com.cobblemon.mod.common.client.net.battle.BattleApplyPassResponseHandler
import com.cobblemon.mod.common.client.net.battle.BattleCaptureEndHandler
import com.cobblemon.mod.common.client.net.battle.BattleCaptureShakeHandler
@@ -37,8 +38,10 @@ import com.cobblemon.mod.common.client.net.callback.party.OpenPartyCallbackHandl
import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallbackHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.UnlockReloadPacketHandler
import com.cobblemon.mod.common.client.net.effect.RunPosableMoLangHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueClosedHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormEntityParticleHandler
import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
@@ -48,7 +51,6 @@ import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonUnpasturedHandler
import com.cobblemon.mod.common.client.net.pokemon.update.PokemonUpdatePacketHandler
import com.cobblemon.mod.common.client.net.settings.ServerSettingsPacketHandler
import com.cobblemon.mod.common.client.net.sound.PokemonCryHandler
import com.cobblemon.mod.common.client.net.sound.UnvalidatedPlaySoundS2CPacketHandler
import com.cobblemon.mod.common.client.net.spawn.SpawnExtraDataEntityHandler
import com.cobblemon.mod.common.client.net.starter.StarterUIPacketHandler
@@ -73,6 +75,7 @@ import com.cobblemon.mod.common.client.net.trade.TradeOfferNotificationHandler
import com.cobblemon.mod.common.client.net.trade.TradeStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeUpdatedHandler
import com.cobblemon.mod.common.net.messages.client.PlayerInteractOptionsPacket
import com.cobblemon.mod.common.net.messages.client.animation.PlayPoseableAnimationPacket
import com.cobblemon.mod.common.net.messages.client.battle.*
import com.cobblemon.mod.common.net.messages.client.callback.OpenMoveCallbackPacket
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyCallbackPacket
@@ -85,6 +88,8 @@ import com.cobblemon.mod.common.net.messages.client.data.SpeciesRegistrySyncPack
import com.cobblemon.mod.common.net.messages.client.data.UnlockReloadPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormParticlePacket
import com.cobblemon.mod.common.net.messages.client.fossil.FossilRegistrySyncPacket
import com.cobblemon.mod.common.net.messages.client.fossil.NaturalMaterialRegistrySyncPacket
@@ -97,7 +102,6 @@ import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.Add
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.ClearEvolutionsPacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.RemoveEvolutionPacket
import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacket
import com.cobblemon.mod.common.net.messages.client.sound.PokemonCryPacket
import com.cobblemon.mod.common.net.messages.client.sound.UnvalidatedPlaySoundS2CPacket
import com.cobblemon.mod.common.net.messages.client.spawn.SpawnGenericBedrockPacket
import com.cobblemon.mod.common.net.messages.client.spawn.SpawnPokeballPacket
@@ -315,6 +319,8 @@ object CobblemonNetwork : NetworkManager {
// Effects
this.createClientBound(SpawnSnowstormParticlePacket.ID, SpawnSnowstormParticlePacket::decode, SpawnSnowstormParticleHandler)
this.createClientBound(SpawnSnowstormEntityParticlePacket.ID, SpawnSnowstormEntityParticlePacket::decode, SpawnSnowstormEntityParticleHandler)
this.createClientBound(RunPosableMoLangPacket.ID, RunPosableMoLangPacket::decode, RunPosableMoLangHandler)
// Hax
this.createClientBound(UnvalidatedPlaySoundS2CPacket.ID, UnvalidatedPlaySoundS2CPacket::decode, UnvalidatedPlaySoundS2CPacketHandler)
@@ -339,7 +345,7 @@ object CobblemonNetwork : NetworkManager {
this.createClientBound(PokemonUnpasturedPacket.ID, PokemonUnpasturedPacket::decode, PokemonUnpasturedHandler)
// Behaviours
this.createClientBound(PokemonCryPacket.ID, PokemonCryPacket::decode, PokemonCryHandler)
this.createClientBound(PlayPoseableAnimationPacket.ID, PlayPoseableAnimationPacket::decode, PlayPoseableAnimationHandler)
// Move select packets
this.createClientBound(OpenMoveCallbackPacket.ID, OpenMoveCallbackPacket::decode, OpenMoveCallbackHandler)
@@ -129,6 +129,43 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, RegistryKey<Regi
@JvmField
val TUMBLESTONE_STEP = this.create("tumblestone.step")
@JvmField
val IMPACT_NORMAL = this.create("impact.normal")
@JvmField
val IMPACT_BUG = this.create("impact.bug")
@JvmField
val IMPACT_DARK = this.create("impact.dark")
@JvmField
val IMPACT_DRAGON = this.create("impact.dragon")
@JvmField
val IMPACT_ELECTRIC = this.create("impact.electric")
@JvmField
val IMPACT_FAIRY = this.create("impact.fairy")
@JvmField
val IMPACT_FIGHTING = this.create("impact.fighting")
@JvmField
val IMPACT_FIRE = this.create("impact.fire")
@JvmField
val IMPACT_FLYING = this.create("impact.flying")
@JvmField
val IMPACT_GHOST = this.create("impact.ghost")
@JvmField
val IMPACT_GRASS = this.create("impact.grass")
@JvmField
val IMPACT_GROUND = this.create("impact.ground")
@JvmField
val IMPACT_ICE = this.create("impact.ice")
@JvmField
val IMPACT_POISON = this.create("impact.poison")
@JvmField
val IMPACT_PSYCHIC = this.create("impact.psychic")
@JvmField
val IMPACT_ROCK = this.create("impact.rock")
@JvmField
val IMPACT_STEEL = this.create("impact.steel")
@JvmField
val IMPACT_WATER = this.create("impact.water")
@JvmField
val TUMBLESTONE_SOUNDS = BlockSoundGroup(1f, 1f,
TUMBLESTONE_BREAK,
@@ -8,9 +8,13 @@
package com.cobblemon.mod.common.api.battles.model
import com.bedrockk.molang.runtime.MoLangEnvironment
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.Environment
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
@@ -18,6 +22,8 @@ import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.api.battles.model.actor.FleeableBattleActor
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunction
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.text.red
@@ -464,4 +470,13 @@ open class PokemonBattle(
LOGGER.error("Missing interpretation on '{}' action: \nPublic » {}\nPrivate » {}", publicMessage.id, publicMessage.rawMessage, privateMessage.rawMessage)
return Text.literal("Missing interpretation on '${publicMessage.id}' action please report to the developers").red()
}
fun addQueryFunctions(queryStruct: QueryStruct): QueryStruct {
queryStruct.addFunction("pvp") { DoubleValue(isPvP) }
queryStruct.addFunction("pvn") { DoubleValue(isPvN) }
queryStruct.addFunction("pvw") { DoubleValue(isPvW) }
queryStruct.addFunction("has_rule") { params -> DoubleValue(params.getString(0) in format.ruleSet) }
return queryStruct
}
}
@@ -19,7 +19,8 @@ import com.bedrockk.molang.runtime.value.MoValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.dialogue.PlayerDialogueFaceProvider
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunction
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunctions
import com.cobblemon.mod.common.util.isInt
import com.cobblemon.mod.common.util.itemRegistry
import net.minecraft.block.Block
@@ -51,7 +52,30 @@ object MoLangFunctions {
"is_number" to java.util.function.Function { params -> DoubleValue(params.get<MoValue>(0).asString().toDoubleOrNull() != null) },
"to_number" to java.util.function.Function { params -> DoubleValue(params.get<MoValue>(0).asString().toDoubleOrNull() ?: 0.0) },
"to_int" to java.util.function.Function { params -> DoubleValue(params.get<MoValue>(0).asString().toIntOrNull() ?: 0) },
"to_string" to java.util.function.Function { params -> StringValue(params.get<MoValue>(0).asString()) }
"to_string" to java.util.function.Function { params -> StringValue(params.get<MoValue>(0).asString()) },
"do_effect_walks" to java.util.function.Function { _ ->
DoubleValue(Cobblemon.config.walkingInBattleAnimations)
},
"random" to java.util.function.Function { params ->
val options = mutableListOf<MoValue>()
var index = 0
while (params.contains(index)) {
options.add(params.get(index))
index++
}
return@Function options.random() // Can throw an exception if they specified no args. They'd be idiots though.
},
"curve" to java.util.function.Function { params ->
val curveName = params.getString(0)
val curve = WaveFunctions.functions[curveName] ?: throw IllegalArgumentException("Unknown curve: $curveName")
return@Function ObjectValue(curve)
},
"array" to java.util.function.Function { params ->
val values = params.params
val array = ArrayStruct(hashMapOf())
values.forEachIndexed { index, moValue -> array.setDirectly("$index", moValue) }
return@Function array
}
)
val biomeFunctions = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
val worldFunctions = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
@@ -110,6 +134,11 @@ object MoLangFunctions {
return this
}
fun <T : QueryStruct> T.addFunction(name: String, function: java.util.function.Function<MoParams, Any>): T {
this.functions[name] = function
return this
}
fun MoLangEnvironment.getQueryStruct(name: String = "query") = structs.getOrPut(name) { QueryStruct(hashMapOf()) } as QueryStruct
fun MoLangRuntime.setup(): MoLangRuntime {
@@ -8,6 +8,12 @@
package com.cobblemon.mod.common.api.moves
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.molang.MoLangFunctions.addFunction
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
import com.cobblemon.mod.common.api.types.ElementalType
@@ -45,8 +51,22 @@ open class MoveTemplate(
val pp: Int,
val priority: Int,
val critRatio: Double,
val effectChances: Array<Double>
val effectChances: Array<Double>,
val actionEffect: ActionEffectTimeline?
) {
val struct: MoStruct by lazy {
QueryStruct(hashMapOf())
.addFunction("name") { StringValue(name) }
.addFunction("type") { StringValue(elementalType.name) }
.addFunction("damage_category") { StringValue(damageCategory.name) }
.addFunction("power") { DoubleValue(power) }
.addFunction("target") { StringValue(target.name) }
.addFunction("accuracy") { DoubleValue(accuracy) }
.addFunction("pp") { DoubleValue(pp) }
.addFunction("priority") { DoubleValue(priority) }
.addFunction("crit_ratio") { DoubleValue(critRatio) }
}
val displayName: MutableText
get() = lang("move.$name")
@@ -65,7 +85,8 @@ open class MoveTemplate(
pp = 5,
priority = 0,
critRatio = 0.0,
effectChances = emptyArray()
effectChances = emptyArray(),
actionEffect = null
)
companion object {
@@ -10,12 +10,15 @@ package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.data.MovesRegistrySyncPacket
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.JsonArray
import com.google.gson.JsonObject
@@ -72,7 +75,18 @@ object Moves : DataRegistry {
effectChances += secondaryMember.get("chance").asDouble
}
}
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
val actionEffect = ActionEffects.actionEffects[id.asIdentifierDefaultingNamespace()]
?: run {
ActionEffects.actionEffects["generic_move".asIdentifierDefaultingNamespace()]
// if (damageCategory == DamageCategories.STATUS) {
// ActionEffects.actionEffects[cobblemonResource("status")]
// } else {
// val type = elementalType.name.lowercase()
// val category = damageCategory.name.lowercase()
// ActionEffects.actionEffects["${category}_$type".asIdentifierDefaultingNamespace()]
// }
}
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray(), actionEffect)
this.register(move)
} catch (e: Exception) {
Cobblemon.LOGGER.error("Caught exception trying to resolve the move '{}'", id, e)
@@ -0,0 +1,94 @@
/*
* 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.moves.animations
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.struct.VariableStruct
import com.cobblemon.mod.common.api.moves.animations.keyframes.ActionEffectKeyframe
import com.cobblemon.mod.common.api.scheduling.ScheduledTask
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.resolveBoolean
import java.util.concurrent.CompletableFuture
import net.minecraft.entity.Entity
/**
* An action effect will run and execute a series of 'keyframes', with each running once
* the previous has completed. The entire effect is considered complete for the given context
* once all the 'holds' provided in the context have been cleared or once every keyframe
* has finished.
*
* @author Hiroku
* @since October 20th, 2023
*/
class ActionEffectTimeline(
val timeline: List<ActionEffectKeyframe> = mutableListOf(),
val condition: Expression = "true".asExpression()
) {
companion object {
val NONE = ActionEffectTimeline()
}
fun run(context: ActionEffectContext): CompletableFuture<Unit> {
return if (timeline.isEmpty() || !context.runtime.resolveBoolean(condition)) {
CompletableFuture.completedFuture(Unit)
} else {
val finalFuture = CompletableFuture<Unit>()
// .toList copy because I'm paranoid about iterators being trying to share between identical effects playing
chainKeyframes(context, timeline.toList().iterator(), finalFuture)
finalFuture
}
}
fun chainKeyframes(context: ActionEffectContext, iterator: Iterator<ActionEffectKeyframe>, finalFuture: CompletableFuture<Unit>) {
if (!iterator.hasNext()) {
finalFuture.complete(Unit)
} else {
val keyframe = iterator.next()
context.currentKeyframes.add(keyframe)
keyframe.play(context)
.thenRun { context.currentKeyframes.remove(keyframe) }
.thenApply { chainKeyframes(context, iterator, finalFuture) }
}
}
}
class ActionEffectContext(
val actionEffect: ActionEffectTimeline,
val holds: MutableSet<String> = mutableSetOf(),
val providers: MutableList<Any> = mutableListOf(),
val runtime: MoLangRuntime,
var canBeInterrupted: Boolean = false,
var interrupted: Boolean = false,
var currentKeyframes: MutableList<ActionEffectKeyframe> = mutableListOf()
) {
inline fun <reified T> findOneProvider() = providers.filterIsInstance<T>().firstOrNull()
}
class UsersProvider(users: List<Entity>): EntityProvider {
override val entities = users
constructor(vararg users: Entity): this(users.toList())
}
class TargetsProvider(targets: List<Entity>): EntityProvider {
override val entities = targets
constructor(vararg targets: Entity): this(targets.toList())
}
interface EntityProvider {
val entities: List<Entity>
}
//class MoveAnimationKeyframe(
// val sound: Identifier? = null,
// val animation: List<AnimationWithMoments> = listOf(),
// val effects: List<ActionEffect> = listOf()
//)
@@ -0,0 +1,95 @@
/*
* 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.moves.animations
import com.bedrockk.molang.Expression
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.keyframes.*
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.pokemon.adapters.CobblemonStatTypeAdapter
import com.cobblemon.mod.common.util.adapters.*
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import java.awt.Color
import net.minecraft.predicate.NumberRange
import net.minecraft.resource.ResourceType
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.minecraft.util.math.Box
import net.minecraft.util.math.Vec3d
/**
* Data registry containing all of the [ActionEffectTimeline]s that can be triggered from various actions.
*
* @author Hiroku
* @since October 21st, 2023
*/
object ActionEffects : JsonDataRegistry<ActionEffectTimeline> {
override val id: Identifier = cobblemonResource("action_effects")
override val type: ResourceType = ResourceType.SERVER_DATA
override val observable = SimpleObservable<ActionEffects>()
init {
ActionEffectKeyframe.register<AnimationActionEffectKeyframe>("animation")
ActionEffectKeyframe.register<EntityMoLangActionEffectKeyframe>("entity_molang")
ActionEffectKeyframe.register<MoLangActionEffectKeyframe>("molang")
ActionEffectKeyframe.register<ParallelActionEffectKeyframe>("parallel")
ActionEffectKeyframe.register<CanInterruptActionEffectKeyframe>("can_interrupt")
ActionEffectKeyframe.register<CannotInterruptActionEffectKeyframe>("cannot_interrupt")
ActionEffectKeyframe.register<RemoveHoldsActionEffectKeyframe>("remove_holds")
ActionEffectKeyframe.register<AddHoldsActionEffectKeyframe>("add_holds")
ActionEffectKeyframe.register<MoveToTargetActionEffectKeyframe>("move_to_target")
ActionEffectKeyframe.register<ReturnToPositionActionEffectKeyframe>("return_to_position")
ActionEffectKeyframe.register<PauseActionEffectKeyframe>("pause")
ActionEffectKeyframe.register<SavePositionActionEffectKeyframe>("save_position")
ActionEffectKeyframe.register<ForkActionEffectKeyframe>("fork")
ActionEffectKeyframe.register<SequenceActionEffectKeyframe>("sequence")
ActionEffectKeyframe.register<RunActionEffectKeyframe>("run_action_effect")
ActionEffectKeyframe.register<EntityParticlesActionEffectKeyframe>("entity_particles")
ActionEffectKeyframe.register<EntitySoundActionEffectKeyframe>("entity_sound")
}
override val gson = GsonBuilder()
.disableHtmlEscaping()
.setPrettyPrinting()
.registerTypeAdapter(ActionEffectKeyframe::class.java, ActionEffectKeyframeAdapter)
.registerTypeAdapter(NumberRange.FloatRange::class.java, FloatNumberRangeAdapter)
.registerTypeAdapter(TypeToken.getParameterized(Collection::class.java, Box::class.java).type, BoxCollectionAdapter)
.registerTypeAdapter(Box::class.java, BoxAdapter)
.registerTypeAdapter(Vec3d::class.java, VerboseVec3dAdapter)
.registerTypeAdapter(Identifier::class.java, IdentifierAdapter)
.registerTypeAdapter(IntRange::class.java, VerboseIntRangeAdapter)
.registerTypeAdapter(Color::class.java, LiteralHexColorAdapter)
.registerTypeAdapter(Stat::class.java, CobblemonStatTypeAdapter)
.registerTypeAdapter(Expression::class.java, ExpressionAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.registerTypeAdapter(
TypeToken.getParameterized(
TypeToken.get(List::class.java).type,
TypeToken.get(ActionEffectKeyframe::class.java).type
).type,
SingleToPluralAdapter(ActionEffectKeyframe::class.java) { it }
)
.create()
override val typeToken: TypeToken<ActionEffectTimeline> = TypeToken.get(ActionEffectTimeline::class.java)
override val resourcePath = "action_effects"
val actionEffects = mutableMapOf<Identifier, ActionEffectTimeline>()
override fun reload(data: Map<Identifier, ActionEffectTimeline>) {
actionEffects.clear()
actionEffects.putAll(data)
observable.emit(this)
}
override fun sync(player: ServerPlayerEntity) {}
}
@@ -0,0 +1,34 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import java.util.concurrent.CompletableFuture
/**
* A single component of an action effect. When played it must return a completable future that completes
* when the action this keyframe represents is finished. [interrupt] is provided for when something successfully
* interrupts the effect so that if any cancellation logic is necessary, it can be provided.
*
* @author Hiroku
* @since October 26th, 2023
*/
interface ActionEffectKeyframe {
companion object {
val types = mutableMapOf<String, Class<out ActionEffectKeyframe>>()
inline fun <reified T : ActionEffectKeyframe> register(type: String) {
types[type] = T::class.java
}
}
fun play(context: ActionEffectContext): CompletableFuture<Unit>
fun interrupt(context: ActionEffectContext) {}
fun skip(): CompletableFuture<Unit> = CompletableFuture.completedFuture(Unit)
}
@@ -0,0 +1,27 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import java.util.concurrent.CompletableFuture
/**
* An [ActionEffectKeyframe] that adds some holds from the context for future code to recognize as blocking some
* functionality from proceeding.
*
* @author Hiroku
* @since October 27th, 2023
*/
class AddHoldsActionEffectKeyframe : ActionEffectKeyframe {
val holds = mutableSetOf<String>()
override fun play(context: ActionEffectContext): CompletableFuture<Unit> {
context.holds.addAll(holds)
return CompletableFuture.completedFuture(Unit)
}
}
@@ -0,0 +1,54 @@
/*
* 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.moves.animations.keyframes
import com.bedrockk.molang.Expression
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.EntityProvider
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.api.scheduling.delayedFuture
import com.cobblemon.mod.common.net.messages.client.animation.PlayPoseableAnimationPacket
import com.cobblemon.mod.common.util.asExpressionLike
import java.util.concurrent.CompletableFuture
import net.minecraft.server.world.ServerWorld
class AnimationActionEffectKeyframe : ConditionalActionEffectKeyframe(), EntityConditionalActionEffectKeyframe {
var delay = "0".asExpressionLike()
var visibilityRange = 200
var animation: Set<String> = setOf("physical")
var variables: List<Expression> = listOf()
override val entityCondition = "q.entity.is_user".asExpressionLike()
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
val entities = context.providers
.filterIsInstance<EntityProvider>()
.flatMap { prov -> prov.entities.filter { test(context, it, isUser = prov is UsersProvider) } }
val expressions = variables.map { it.originalString }.toSet()
// Treat them as expressions if possible but otherwise yeah just send them as strings
val animation = animation.map {
try {
it.asExpressionLike().resolveString(context.runtime).takeIf { it != "0.0" } ?: it
} catch (e: Exception) {
it
}
}.toSet()
for (entity in entities) {
val world = entity.world as ServerWorld
val players = world.getPlayers { it.distanceTo(entity) <= visibilityRange }
val pkt = PlayPoseableAnimationPacket(entity.id, animation = animation, expressions = expressions)
players.forEach { it.sendPacket(pkt) }
}
return delayedFuture(seconds = delay.resolveFloat(context.runtime), serverThread = true)
}
}
@@ -0,0 +1,19 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import java.util.concurrent.CompletableFuture
class CanInterruptActionEffectKeyframe : ConditionalActionEffectKeyframe() {
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
context.canBeInterrupted = true
return CompletableFuture.completedFuture(Unit)
}
}
@@ -0,0 +1,19 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import java.util.concurrent.CompletableFuture
class CannotInterruptActionEffectKeyframe : ConditionalActionEffectKeyframe() {
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
context.canBeInterrupted = true
return CompletableFuture.completedFuture(Unit)
}
}
@@ -0,0 +1,27 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.util.asExpressionLike
import java.util.concurrent.CompletableFuture
abstract class ConditionalActionEffectKeyframe : ActionEffectKeyframe {
var condition: ExpressionLike = "true".asExpressionLike()
override fun play(context: ActionEffectContext): CompletableFuture<Unit> {
return if (condition.resolveBoolean(context.runtime)) {
playWhenTrue(context)
} else {
skip()
}
}
abstract fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit>
}
@@ -0,0 +1,36 @@
/*
* 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.moves.animations.keyframes
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.molang.ExpressionLike
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import net.minecraft.entity.Entity
/**
* An action effect keyframe that plays for all entities for which the condition is true.
*
* @author Hiroku
* @since January 21st, 2024
*/
interface EntityConditionalActionEffectKeyframe {
val entityCondition: ExpressionLike
fun test(context: ActionEffectContext, entity: Entity, isUser: Boolean): Boolean {
// TODO this should use the entity's own query struct
context.runtime.environment.getQueryStruct().addFunction("entity") {
QueryStruct(hashMapOf())
.addFunction("uuid") { StringValue(entity.uuidAsString) }
.addFunction("is_user") { DoubleValue(isUser) }
}
return entityCondition.resolveBoolean(context.runtime)
}
}
@@ -0,0 +1,51 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.EntityProvider
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.api.scheduling.delayedFuture
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
import com.cobblemon.mod.common.util.asExpressionLike
import java.util.concurrent.CompletableFuture
import net.minecraft.server.world.ServerWorld
/**
* An action effect keyframe that will run client-side MoLang for all entities that fit the entity condition.
*
* This MoLang executes within that entity's runtime, meaning you can trigger animations or locational particles
* or sounds.
*
* @author Hiroku
* @since October 30th, 2023
*/
class EntityMoLangActionEffectKeyframe : ConditionalActionEffectKeyframe(), EntityConditionalActionEffectKeyframe {
var delay = "0".asExpressionLike()
val expressions = mutableSetOf<String>()
val visibilityRange = 200
override val entityCondition: ExpressionLike = "q.entity.is_user".asExpressionLike()
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
val entities = context.providers
.filterIsInstance<EntityProvider>()
.flatMap { prov -> prov.entities.filter { test(context, it, isUser = prov is UsersProvider) } }
for (entity in entities) {
val world = entity.world as ServerWorld
val players = world.getPlayers { it.distanceTo(entity) <= visibilityRange }
val pkt = RunPosableMoLangPacket(entityId = entity.id, expressions = expressions)
players.forEach { it.sendPacket(pkt) }
}
return delayedFuture(seconds = delay.resolveFloat(context.runtime), serverThread = true)
}
}
@@ -0,0 +1,56 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.EntityProvider
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.api.scheduling.delayedFuture
import com.cobblemon.mod.common.entity.Poseable
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import java.util.concurrent.CompletableFuture
import net.minecraft.server.world.ServerWorld
/**
* Spawns particles on the entity based on the given particle effect and locator.
*
* @author Hiroku
* @since January 21st, 2024
*/
class EntityParticlesActionEffectKeyframe : ConditionalActionEffectKeyframe(), EntityConditionalActionEffectKeyframe {
override val entityCondition = "q.entity.is_user".asExpressionLike()
var effect: String? = null
var locator: String = "root"
val delay: ExpressionLike = "0".asExpressionLike()
val visibilityRange = 200
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
val entities = context.providers
.filterIsInstance<EntityProvider>()
.flatMap { prov -> prov.entities.filter { test(context, it, isUser = prov is UsersProvider) } }
val effectIdentifier = try {
effect?.asExpressionLike()?.resolveString(context.runtime)
} catch (e: Exception) {
effect
}?.asIdentifierDefaultingNamespace() ?: return skip()
entities.filter { it is Poseable }.forEach { entity ->
val packet = SpawnSnowstormEntityParticlePacket(effectIdentifier, entity.id, locator)
val players = (entity.world as ServerWorld).getPlayers { it.distanceTo(entity) <= visibilityRange }
packet.sendToPlayers(players)
}
return delayedFuture(seconds = delay.resolveFloat(context.runtime), serverThread = true)
}
}
@@ -0,0 +1,50 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.EntityProvider
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.api.scheduling.delayedFuture
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import java.util.concurrent.CompletableFuture
import net.minecraft.registry.RegistryKeys
/**
* An action effect keyframe that plays a positioned sound for all matching entities.
*
* @author Hiroku
* @since January 21st, 2024
*/
class EntitySoundActionEffectKeyframe : ConditionalActionEffectKeyframe(), EntityConditionalActionEffectKeyframe {
override val entityCondition = "q.entity.is_user".asExpressionLike()
var sound: String? = null
val delay: ExpressionLike = "0".asExpressionLike()
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
val entities = context.providers
.filterIsInstance<EntityProvider>()
.flatMap { prov -> prov.entities.filter { test(context, it, isUser = prov is UsersProvider) } }
val soundIdentifier = try {
sound?.asExpressionLike()?.resolveString(context.runtime)?.takeIf { it != "0.0" } ?: sound
} catch (e: Exception) {
sound
}?.asIdentifierDefaultingNamespace() ?: return skip()
entities.forEach { entity ->
val soundEvent = entity.world.registryManager.get(RegistryKeys.SOUND_EVENT).get(soundIdentifier) ?: return skip()
entity.playSound(soundEvent, 1f, 1f)
}
return delayedFuture(seconds = delay.resolveFloat(context.runtime), serverThread = true)
}
}
@@ -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.moves.animations.keyframes
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.util.asExpressionLike
import java.util.concurrent.CompletableFuture
class ForkActionEffectKeyframe(
val condition: ExpressionLike = "true".asExpressionLike(),
val ifTrue: List<ActionEffectKeyframe> = listOf(),
val ifFalse: List<ActionEffectKeyframe> = listOf()
) : ActionEffectKeyframe {
override fun play(context: ActionEffectContext): CompletableFuture<Unit> {
val future = CompletableFuture<Unit>()
/*
* Using .toList() before running chainKeyframes because iterators can be weird about sharing,
* .toList() creates a copy of the list and then grabs its iterator so much safer (three parachutes)
*/
if (!condition.resolveBoolean(context.runtime)) {
context.actionEffect.chainKeyframes(context, ifFalse.toList().iterator(), future)
} else {
context.actionEffect.chainKeyframes(context, ifTrue.toList().iterator(), future)
}
return future
}
}
@@ -0,0 +1,30 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.scheduling.delayedFuture
import com.cobblemon.mod.common.util.asExpressionLike
import java.util.concurrent.CompletableFuture
/**
* An action effect keyframe that simply executes some MoLang on the effect context's runtime.
*
* @author Hiroku
* @since January 21st, 2024
*/
class MoLangActionEffectKeyframe : ActionEffectKeyframe {
val expressions: ExpressionLike = "0".asExpressionLike()
val delay: ExpressionLike = "0".asExpressionLike()
override fun play(context: ActionEffectContext): CompletableFuture<Unit> {
expressions.resolve(context.runtime)
return delayedFuture(seconds = delay.resolveFloat(context.runtime), serverThread = true)
}
}
@@ -0,0 +1,75 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.api.scheduling.afterOnServer
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonNavigation
import com.cobblemon.mod.common.util.asExpressionLike
import java.util.concurrent.CompletableFuture
import kotlin.math.pow
import kotlin.math.sqrt
import net.minecraft.util.Identifier
class MoveToTargetActionEffectKeyframe : ActionEffectKeyframe {
val speed = 1F
val timeout = "4".asExpressionLike()
var proximity = -1F
val timeoutActionEffect: Identifier? = null
override fun play(context: ActionEffectContext): CompletableFuture<Unit> {
val user = context.findOneProvider<UsersProvider>()?.entities?.firstOrNull() as? PokemonEntity ?: return CompletableFuture.completedFuture(Unit)
val target = context.findOneProvider<TargetsProvider>()?.entities?.firstOrNull() ?: return CompletableFuture.completedFuture(Unit)
val future = CompletableFuture<Unit>()
var timedOut = false
val proximity = this.proximity.takeIf { it != -1F } ?: (sqrt(2 * user.boundingBox.xLength.pow(2)) + 1.5F + sqrt(2 * target.boundingBox.xLength.pow(2))).toFloat()
if (target.distanceTo(user) !in proximity..20F) {
future.complete(Unit)
return future
}
val timeoutEffect = timeoutActionEffect?.let { ActionEffects.actionEffects[it] } ?: ActionEffectTimeline.NONE
val nav = user.navigation
val navContext = PokemonNavigation.NavigationContext(
destinationProximity = proximity,
onArrival = {
if (!future.isDone && !timedOut) {
future.complete(Unit)
}
},
onCannotReach = {
if (!future.isDone && !timedOut) {
timedOut = true
timeoutEffect.run(context).thenApply { future.complete(Unit) }
}
}
)
afterOnServer(seconds = timeout.resolveFloat(context.runtime)) {
if (!future.isDone && !timedOut) {
timedOut = true
timeoutEffect.run(context).thenApply { future.complete(Unit) }
nav.stop()
}
}
nav.startMovingTo(target.x, target.y, target.z, speed.toDouble(), navContext)
return future
}
}
@@ -0,0 +1,25 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import java.util.concurrent.CompletableFuture
class ParallelActionEffectKeyframe : ConditionalActionEffectKeyframe() {
var keyframes: MutableList<ActionEffectKeyframe> = mutableListOf()
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
return CompletableFuture.allOf(
*keyframes.map {
context.currentKeyframes.add(it)
it.play(context).thenRun { context.currentKeyframes.remove(it) }
}.toTypedArray()
).thenApply {} // Map Void -> Unit at the end
}
}
@@ -0,0 +1,18 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.scheduling.delayedFuture
import com.cobblemon.mod.common.util.asExpressionLike
class PauseActionEffectKeyframe : ConditionalActionEffectKeyframe() {
val pause = "1".asExpressionLike()
override fun playWhenTrue(context: ActionEffectContext) = delayedFuture(seconds = pause.resolveFloat(context.runtime), serverThread = true)
}
@@ -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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.scheduling.delayedFuture
import java.util.concurrent.CompletableFuture
/**
* An [ActionEffectKeyframe] that clears some holds from the context, potentially marking the effect as
* complete as far as any blocked actions are concerned. If no holds are specified, then all holds are
* removed.
*
* @author Hiroku
* @since October 27th, 2023
*/
class RemoveHoldsActionEffectKeyframe : ActionEffectKeyframe {
val delay = 0F
val holds = mutableSetOf<String>()
override fun play(context: ActionEffectContext): CompletableFuture<Unit> {
return delayedFuture(seconds = delay, serverThread = true).thenApply {
if (holds.isEmpty()) {
context.holds.clear()
} else {
context.holds.removeAll(holds)
}
}
}
}
@@ -0,0 +1,70 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.api.scheduling.after
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonNavigation
import com.cobblemon.mod.common.util.asExpressionLike
import java.util.concurrent.CompletableFuture
import net.minecraft.util.Identifier
import net.minecraft.util.math.Vec3d
class ReturnToPositionActionEffectKeyframe : ActionEffectKeyframe {
val speed = 1F
val timeout = "4".asExpressionLike()
val timeoutActionEffect: Identifier? = null
override fun play(context: ActionEffectContext): CompletableFuture<Unit> {
val user = context.findOneProvider<UsersProvider>()?.entities?.firstOrNull() as? PokemonEntity ?: return skip()
val future = CompletableFuture<Unit>()
val pos = (context.runtime.environment.getValue(setOf("${user.uuidAsString}-pos").iterator())?.value() as? ObjectValue<*>)?.obj as? Vec3d ?: return skip()
var timedOut = false
if (pos.distanceTo(user.pos) > 20) {
future.complete(Unit)
return future
}
val timeoutEffect = timeoutActionEffect?.let { ActionEffects.actionEffects[it] } ?: ActionEffectTimeline.NONE
val nav = user.navigation
val navContext = PokemonNavigation.NavigationContext(
onArrival = {
if (!future.isDone && !timedOut) {
future.complete(Unit)
}
},
onCannotReach = {
if (!future.isDone && !timedOut) {
timedOut = true
timeoutEffect.run(context).thenApply { future.complete(Unit) }
}
}
)
after(seconds = timeout.resolveFloat(context.runtime), serverThread = true) {
if (!future.isDone && !timedOut) {
timedOut = true
timeoutEffect.run(context).thenApply { future.complete(Unit) }
nav.stop()
}
}
nav.startMovingTo(pos.x, pos.y, pos.z, speed.toDouble(), navContext)
return future
}
}
@@ -0,0 +1,34 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import java.util.concurrent.CompletableFuture
import net.minecraft.util.Identifier
/**
* An action effect keyframe that simply runs another action effect based on an identifier. This action
* effect is marked complete only when the action effect it runs is complete unless [waitForActionEffect] is
* false.
*
* @author Hiroku
* @since January 21st, 2024
*/
class RunActionEffectKeyframe : ConditionalActionEffectKeyframe() {
val actionEffect: Identifier? = null
val waitForActionEffect = true
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
return if (actionEffect != null) {
ActionEffects.actionEffects[actionEffect]?.run(context)?.takeIf { waitForActionEffect } ?: skip()
} else {
skip()
}
}
}
@@ -0,0 +1,22 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import java.util.concurrent.CompletableFuture
class SavePositionActionEffectKeyframe: ConditionalActionEffectKeyframe() {
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
val user = context.findOneProvider<UsersProvider>()?.entities?.firstOrNull() ?: return skip()
context.runtime.environment.setSimpleVariable("${user.uuidAsString}-pos", ObjectValue(user.pos))
return skip()
}
}
@@ -0,0 +1,26 @@
/*
* 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.moves.animations.keyframes
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import java.util.concurrent.CompletableFuture
/**
* A sequence of action effects that are dependent on a condition.
*
* @author Hiroku
* @since October 29th, 2023
*/
class SequenceActionEffectKeyframe(val keyframes: List<ActionEffectKeyframe> = listOf()) : ConditionalActionEffectKeyframe() {
override fun playWhenTrue(context: ActionEffectContext): CompletableFuture<Unit> {
val future = CompletableFuture<Unit>()
context.actionEffect.chainKeyframes(context, keyframes.toList().iterator(), future)
return future
}
}
@@ -8,13 +8,27 @@
package com.cobblemon.mod.common.api.scheduling
import java.util.concurrent.CompletableFuture
@Deprecated("Use afterOnServer or afterOnClient; ambiguous side is not good for your health")
@JvmOverloads
fun after(ticks: Int = 0, seconds: Float = 0F, serverThread: Boolean = false, action: () -> Unit) {
val scheduler = if (serverThread) ServerTaskTracker else ClientTaskTracker
scheduler.after(seconds = seconds + ticks / 20F, action)
}
fun delayedFuture(ticks: Int = 0, seconds: Float = 0F, serverThread: Boolean = false): CompletableFuture<Unit> {
val future = CompletableFuture<Unit>()
if (ticks == 0 && seconds == 0F) {
future.complete(Unit)
} else {
after(ticks = ticks, seconds = seconds, serverThread = serverThread) { future.complete(Unit) }
}
return future
}
/**
* Delayed task is created to run on the main thread. This is for when the task
* being completed after the delay does things like entity removal or other thread-unsafe actions.
@@ -25,6 +25,7 @@ import com.mojang.serialization.DynamicOps
import com.mojang.serialization.codecs.ListCodec
import com.mojang.serialization.codecs.PrimitiveCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import com.mojang.serialization.codecs.UnboundedMapCodec
import kotlin.math.floor
import net.minecraft.network.PacketByteBuf
@@ -44,6 +45,7 @@ interface MoLangCurve : CodecMapped {
registerSubtype(CurveType.LINEAR, LinearMoLangCurve::class.java, LinearMoLangCurve.CODEC)
registerSubtype(CurveType.CATMULL_ROM, CatmullRomMoLangCurve::class.java, CatmullRomMoLangCurve.CODEC)
registerSubtype(CurveType.BEZIER, BezierMoLangCurve::class.java, BezierMoLangCurve.CODEC)
registerSubtype(CurveType.BEZIER_CHAIN, BezierChainMoLangCurve::class.java, BezierChainMoLangCurve.CODEC)
}
}
@@ -164,6 +166,103 @@ class CatmullRomMoLangCurve(
}
}
class BezierChainMoLangCurve(
override var name: String = "variable",
override var input: Expression = NumberExpression(0.5),
var nodes: Map<Double, BezierChainNode> = emptyMap()
) : MoLangCurve {
var nodePairs: List<BezierChainNodePair> = emptyList()
fun deriveNodePairs() {
val nodePairs = mutableListOf<BezierChainNodePair>()
val sortedNodes = nodes.entries.sortedBy { it.key }
for (i in 1 until sortedNodes.size) {
val node1 = sortedNodes[i - 1]
val node2 = sortedNodes[i]
nodePairs.add(BezierChainNodePair(node1.key, node2.key, node1.value, node2.value))
}
this.nodePairs = nodePairs
}
init {
deriveNodePairs()
}
class BezierChainNode(var value: Double, var slope: Double) {
fun writeToBuffer(buffer: PacketByteBuf) {
buffer.writeDouble(value)
buffer.writeDouble(slope)
}
fun readFromBuffer(buffer: PacketByteBuf) {
value = buffer.readDouble()
slope = buffer.readDouble()
}
companion object {
val CODEC = RecordCodecBuilder.create<BezierChainNode> { instance ->
instance.group(
PrimitiveCodec.DOUBLE.fieldOf("value").forGetter { it.value },
PrimitiveCodec.DOUBLE.fieldOf("slope").forGetter { it.slope }
).apply(instance, ::BezierChainNode)
}
}
}
class BezierChainNodePair(
val startTime: Double,
val endTime: Double,
node1: BezierChainNode,
node2: BezierChainNode
) {
val curve: CubedBezierCurve
init {
val v1 = node1.value
val v2 = node1.value + node1.slope / 3
val v3 = node2.value - node2.slope / 3
val v4 = node2.value
curve = CubedBezierCurve(v1, v2, v3, v4)
}
}
class BezierChainPointData(val time: Double, val value: Double, val slope: Double)
companion object {
val CODEC: Codec<BezierChainMoLangCurve> = RecordCodecBuilder.create { instance ->
instance.group(
PrimitiveCodec.STRING.fieldOf("type").forGetter { it.type.name },
PrimitiveCodec.STRING.fieldOf("name").forGetter { it.name },
EXPRESSION_CODEC.fieldOf("input").forGetter { it.input },
UnboundedMapCodec(PrimitiveCodec.DOUBLE, BezierChainNode.CODEC).fieldOf("nodes").forGetter { it.nodes }
).apply(instance) { _, name, input, nodes -> BezierChainMoLangCurve(name, input, nodes) }
}
}
override val type: CurveType = CurveType.BEZIER_CHAIN
override fun resolve(runtime: MoLangRuntime, inputValue: Double): Double {
val position = inputValue.coerceIn(0.0, 1.0)
val nodePair = nodePairs.last { position >= it.startTime }
val curve = nodePair.curve
val t = (position - nodePair.startTime) / (nodePair.endTime - nodePair.startTime)
return curve.getY(t)
}
override fun <T> encode(ops: DynamicOps<T>) = CODEC.encodeStart(ops, this)
override fun readFromBuffer(buffer: PacketByteBuf) {
name = buffer.readString()
input = MoLang.createParser(buffer.readString()).parseExpression()
nodes = buffer.readMap({ buffer.readDouble() }, { BezierChainNode(0.0, 0.0).also { it.readFromBuffer(buffer) } })
deriveNodePairs()
}
override fun writeToBuffer(buffer: PacketByteBuf) {
buffer.writeString(name)
buffer.writeString(input.getString())
buffer.writeMap(nodes, { pb, key -> pb.writeDouble(key) }, { pb, value -> value.writeToBuffer(pb) })
}
}
class BezierMoLangCurve(
override var name: String = "variable",
override var input: Expression = NumberExpression(0.5),
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.data.ArbitrarilyMappedSerializableCompanion
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.util.codec.EXPRESSION_CODEC
import com.cobblemon.mod.common.util.getString
import com.cobblemon.mod.common.util.math.geometry.transformDirection
import com.cobblemon.mod.common.util.resolveDouble
import com.mojang.serialization.Codec
import com.mojang.serialization.DynamicOps
@@ -135,7 +136,13 @@ class InwardsMotionDirection : ParticleMotionDirection {
}
override val type = ParticleMotionDirectionType.INWARDS
override fun getDirectionVector(runtime: MoLangRuntime, storm: ParticleStorm, emitterPos: Vec3d, particlePos: Vec3d) = emitterPos.subtract(particlePos).normalize()
override fun getDirectionVector(runtime: MoLangRuntime, storm: ParticleStorm, emitterPos: Vec3d, particlePos: Vec3d): Vec3d {
return if (particlePos == emitterPos) {
Vec3d(storm.world.random.nextDouble() - 0.5, storm.world.random.nextDouble() - 0.5, storm.world.random.nextDouble() - 0.5)
} else {
particlePos.subtract(emitterPos)
}.normalize()
}
override fun <T> encode(ops: DynamicOps<T>) = CODEC.encodeStart(ops, this)
override fun readFromBuffer(buffer: PacketByteBuf) {}
override fun writeToBuffer(buffer: PacketByteBuf) {}
@@ -151,7 +158,11 @@ class OutwardsMotionDirection : ParticleMotionDirection {
override val type = ParticleMotionDirectionType.OUTWARDS
override fun getDirectionVector(runtime: MoLangRuntime, storm: ParticleStorm, emitterPos: Vec3d, particlePos: Vec3d): Vec3d {
return particlePos.subtract(emitterPos).normalize()
return if (particlePos == emitterPos) {
Vec3d(storm.world.random.nextDouble() - 0.5, storm.world.random.nextDouble() - 0.5, storm.world.random.nextDouble() - 0.5)
} else {
particlePos.subtract(emitterPos)
}.normalize()
}
override fun <T> encode(ops: DynamicOps<T>) = CODEC.encodeStart(ops, this)
@@ -185,7 +196,7 @@ class CustomMotionDirection(
runtime.resolveDouble(direction.second),
runtime.resolveDouble(direction.third)
)
return if (storm.effect.space.localPosition) storm.transformDirection(v) else v
return storm.matrixWrapper.matrix.transformDirection(v)
}
override fun <T> encode(ops: DynamicOps<T>) = CODEC.encodeStart(ops, this)
@@ -39,15 +39,7 @@ import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.battles.dispatch.WaitDispatch
import com.cobblemon.mod.common.battles.interpreter.ContextManager
import com.cobblemon.mod.common.battles.interpreter.instructions.DamageInstruction
import com.cobblemon.mod.common.battles.interpreter.instructions.DeprecatedInstruction
import com.cobblemon.mod.common.battles.interpreter.instructions.DeprecatedSplitInstruction
import com.cobblemon.mod.common.battles.interpreter.instructions.FaintInstruction
import com.cobblemon.mod.common.battles.interpreter.instructions.IgnoredInstruction
import com.cobblemon.mod.common.battles.interpreter.instructions.MoveInstruction
import com.cobblemon.mod.common.battles.interpreter.instructions.TurnInstruction
import com.cobblemon.mod.common.battles.interpreter.instructions.UnknownInstruction
import com.cobblemon.mod.common.battles.interpreter.instructions.UpkeepInstruction
import com.cobblemon.mod.common.battles.interpreter.instructions.*
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMadeInvalidChoicePacket
@@ -124,12 +116,13 @@ object ShowdownInterpreter {
updateInstructionParser["upkeep"] = { _, _, _, _ -> UpkeepInstruction() }
updateInstructionParser["faint"] = { battle, _, message, _ -> FaintInstruction(battle, message) }
updateInstructionParser["move"] = { _, instructionSet, message, _ -> MoveInstruction(instructionSet, message) }
splitInstructionParser["-damage"] = { _, targetActor, _, publicMessage, privateMessage, _ ->
DamageInstruction(targetActor, publicMessage, privateMessage)
updateInstructionParser["-activate"] = { _, instructionSet, message, _ -> ActivateInstruction(instructionSet, message) }
updateInstructionParser["-ability"] = { _, instructionSet, message, _ -> AbilityInstruction(instructionSet, message) }
updateInstructionParser["-miss"] = { battle, instructionSet, message, _ -> MissInstruction(battle, instructionSet, message) }
splitInstructionParser["-damage"] = { _, targetActor, instructionSet, publicMessage, privateMessage, _ ->
DamageInstruction(instructionSet, targetActor, publicMessage, privateMessage)
}
// Note '-cureteam' is a legacy thing that is only used in generation 2 and 4 mods for heal bell and aromatherapy respectively as such we can just ignore that
updateInstructions["|win|"] = this::handleWinInstruction
@@ -143,11 +136,9 @@ object ShowdownInterpreter {
updateInstructions["|-fail|"] = this::handleFailInstruction
updateInstructions["|-start|"] = this::handleStartInstructions
updateInstructions["|-block|"] = this::handleBlockInstructions
updateInstructions["|-activate|"] = this::handleActivateInstructions
updateInstructions["|-curestatus|"] = this::handleCureStatusInstruction
updateInstructions["|-fieldstart|"] = this::handleFieldStartInstructions
updateInstructions["|-fieldend|"] = this::handleFieldEndInstructions
updateInstructions["|-ability|"] = this::handleAbilityInstructions
updateInstructions["|-endability|"] = this::handleEndAbilityInstruction
updateInstructions["|-nothing"] = { battle, _, _ ->
battle.dispatchGo { battle.broadcastChatMessage(battleLang("nothing")) }
@@ -169,7 +160,6 @@ object ShowdownInterpreter {
updateInstructions["|-invertboost|"] = this::handleInvertBoostInstruction
updateInstructions["|-status|"] = this::handleStatusInstruction
updateInstructions["|-end|"] = this::handleEndInstruction
updateInstructions["|-miss|"] = this::handleMissInstruction
updateInstructions["|-hitcount|"] = this::handleHitCountInstruction
updateInstructions["|-item|"] = this::handleItemInstruction
updateInstructions["|-enditem|"] = this::handleEndItemInstruction
@@ -594,20 +584,6 @@ object ShowdownInterpreter {
}
}
/**
* Format:
* |-miss|SOURCE|TARGET
*
* The move used by the SOURCE Pokémon missed (maybe absent) the TARGET Pokémon.
*/
private fun handleMissInstruction(battle: PokemonBattle, message: BattleMessage, remainingLines: MutableList<String>) {
battle.dispatchWaiting(1.5F) {
val pokemon = message.getBattlePokemon(0, battle) ?: return@dispatchWaiting
battle.broadcastChatMessage(battleLang("missed").red())
battle.minorBattleActions[pokemon.uuid] = message
}
}
/**
* Format:
* |-immune|POKEMON
@@ -663,7 +639,7 @@ object ShowdownInterpreter {
val effectID = message.effectAt(1)?.id ?: return@dispatchWaiting
val name = pokemon.getName()
// Move may be null as it's not always given
val moveName = message.moveAt(2)?.displayName ?: run { println(message.argumentAt(2)); "(Unrecognized: ${message.argumentAt(2)})".text() }
val moveName = message.moveAt(2)?.displayName ?: run { "(Unrecognized: ${message.argumentAt(2)})".text() }
val lang = when (effectID) {
// TODO: in the games they use a generic image because there is a popup of the ability and the sprite of the mon, it may be good to have a similar system here
@@ -995,46 +971,6 @@ object ShowdownInterpreter {
}
}
/**
* Format:
* |-activate|POKEMON|EFFECT
*
* A miscellaneous effect has activated.This is triggered whenever an effect could not be better described by one of the other minor messages.
*/
private fun handleActivateInstructions(battle: PokemonBattle, message: BattleMessage, remainingLines: MutableList<String>){
val pokemon = message.getBattlePokemon(0, battle) ?: return
val pokemonName = pokemon.getName()
val sourceName = message.getSourceBattlePokemon(battle)?.getName() ?: Text.literal("UNKNOWN")
val effect = message.effectAt(1) ?: return
val extraEffect = message.effectAt(2)?.typelessData ?: Text.literal("UNKNOWN")
broadcastOptionalAbility(battle, effect, pokemonName)
battle.dispatch{
this.lastCauser[battle.battleId] = message
battle.minorBattleActions[pokemon.uuid] = message
val lang = when (effect.id) {
// Includes a 3rd argument being the magnitude level as a number
"magnitude" -> battleLang("activate.magnitude", message.argumentAt(2)?.toIntOrNull() ?: 1)
// Includes spited move and the PP it was reduced by
"spite", "eeriespell" -> battleLang("activate.spite", pokemonName, extraEffect, message.argumentAt(3)!!)
// Don't need additional lang, announced elsewhere
"toxicdebris", "shedskin" -> return@dispatch GO
// Add activation to each Pokemon's history
"destinybond" -> {
battle.activePokemon.mapNotNull { it.battlePokemon?.uuid }.forEach { battle.minorBattleActions[it] = message }
battleLang("activate.destinybond", pokemonName)
}
"focussash", "focusband" -> battleLang("activate.focusband", pokemonName, effect.typelessData)
"maxguard", "protect" -> battleLang("activate.protect", pokemonName)
"shadowforce", "hyperspacefury", "hyperspacehole" -> battleLang("activate.phantomforce", pokemonName)
else -> battleLang("activate.${effect.id}", pokemonName, sourceName, extraEffect)
}
battle.broadcastChatMessage(lang)
WaitDispatch(1F)
}
}
/**
* Format:
* |-fieldstart|CONDITION
@@ -1092,54 +1028,6 @@ object ShowdownInterpreter {
}
}
/**
* Format:
* |-ability|POKEMON|ABILITY|(from)EFFECT
*
* The ABILITY of the POKEMON has been changed due to a move/ability EFFECT.
*
* Format:
* |-ability|POKEMON|ABILITY
*
* POKEMON has just switched-in, and its ability ABILITY is being announced to have a long-term effect.
*/
private fun handleAbilityInstructions(battle: PokemonBattle, message: BattleMessage, remainingLines: MutableList<String>) {
val pokemon = message.getBattlePokemon(0, battle) ?: return
val pokemonName = pokemon.getName()
val effect = message.effectAt(1) ?: return
val optionalEffect = message.effect()
val optionalPokemon = message.getSourceBattlePokemon(battle)
val optionalPokemonName = optionalPokemon?.getName()
// If there is an optional effect causing the activation, broadcast that instead of the standard effect
if (optionalEffect != null) {
broadcastAbility(battle, optionalEffect, pokemonName)
} else {
broadcastAbility(battle, effect, pokemonName)
}
battle.dispatch {
this.lastCauser[battle.battleId] = message
val lang = when (optionalEffect?.id) {
"trace" -> optionalPokemonName?.let { battleLang("ability.trace", pokemonName, it, effect.typelessData) }
"receiver", "powerofalchemy" -> optionalPokemonName?.let { battleLang("ability.receiver", it, effect.typelessData) } // Receiver and Power of Alchemy share the same text
else -> when (effect.id) {
"sturdy", "unnerve", "anticipation" -> battleLang("ability.${effect.id}", pokemonName) // Unique message
"airlock", "cloudnine" -> battleLang("ability.airlock") // Cloud Nine shares the same text as Air Lock
else -> null // Effect broadcasted by a succeeding instruction
}
}
battle.minorBattleActions[pokemon.uuid] = message
if (lang != null) {
battle.broadcastChatMessage(lang)
return@dispatch WaitDispatch(1F)
}
else return@dispatch GO
}
}
fun broadcastOptionalAbility(battle: PokemonBattle, effect: Effect?, pokemonName: MutableText) {
if (effect != null && effect.type == Effect.Type.ABILITY)
broadcastAbility(battle, effect, pokemonName)
@@ -0,0 +1,11 @@
/*
* 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.dispatch
interface CauserInstruction
@@ -9,9 +9,11 @@
package com.cobblemon.mod.common.battles.dispatch
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import org.objectweb.asm.tree.analysis.Interpreter
class InstructionSet {
val instructions: MutableList<InterpreterInstruction> = mutableListOf()
fun getSubsequentInstructions(instruction: InterpreterInstruction): List<InterpreterInstruction> {
val index = instructions.indexOf(instruction)
return instructions.subList(index + 1, instructions.size).toList()
@@ -22,6 +24,31 @@ class InstructionSet {
return instructions.subList(0, index).toList()
}
inline fun <reified T> getMostRecentInstruction(comparedTo: InterpreterInstruction, predicate: (T) -> Boolean = { true }): T? {
val index = instructions.indexOf(comparedTo)
return instructions.subList(0, index).filterIsInstance<T>().lastOrNull(predicate)
}
fun findInstructionsCausedBy(causerInstruction: CauserInstruction): List<InterpreterInstruction> {
causerInstruction as InterpreterInstruction
val thisCauseIndex = instructions.indexOf(causerInstruction)
if (thisCauseIndex == instructions.size - 1) {
return emptyList()
}
val nextCauseIndex = getNextInstruction<CauserInstruction>(causerInstruction)?.let { instructions.indexOf(it as InterpreterInstruction) }
return instructions.subList(thisCauseIndex + 1, nextCauseIndex ?: instructions.size)
}
inline fun <reified T> getNextInstruction(comparedTo: InterpreterInstruction, predicate: (T) -> Boolean = { true }): T? {
val index = instructions.indexOf(comparedTo)
if (instructions.last() == comparedTo) {
return null
}
return instructions.subList(index + 1, instructions.size).filterIsInstance<T>().firstOrNull(predicate)
}
fun getMostRecentCauser(comparedTo: InterpreterInstruction) = getMostRecentInstruction<CauserInstruction>(comparedTo)
fun execute(battle: PokemonBattle) {
instructions.forEach { it(battle) }
}
@@ -0,0 +1,69 @@
/*
* 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.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.CauserInstruction
import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.dispatch.WaitDispatch
import com.cobblemon.mod.common.util.battleLang
/**
* Format:
* |-ability|POKEMON|ABILITY|(from)EFFECT
*
* The ABILITY of the POKEMON has been changed due to a move/ability EFFECT.
*
* Format:
* |-ability|POKEMON|ABILITY
*
* POKEMON has just switched-in, and its ability ABILITY is being announced to have a long-term effect.
*/
class AbilityInstruction(val instructionSet: InstructionSet, val message: BattleMessage) : InterpreterInstruction, CauserInstruction {
override fun invoke(battle: PokemonBattle) {
val pokemon = message.getBattlePokemon(0, battle) ?: return
val pokemonName = pokemon.getName()
val effect = message.effectAt(1) ?: return
val optionalEffect = message.effect()
val optionalPokemon = message.getSourceBattlePokemon(battle)
val optionalPokemonName = optionalPokemon?.getName()
// If there is an optional effect causing the activation, broadcast that instead of the standard effect
if (optionalEffect != null) {
ShowdownInterpreter.broadcastAbility(battle, optionalEffect, pokemonName)
} else {
ShowdownInterpreter.broadcastAbility(battle, effect, pokemonName)
}
battle.dispatch {
ShowdownInterpreter.lastCauser[battle.battleId] = message
val lang = when (optionalEffect?.id) {
"trace" -> optionalPokemonName?.let { battleLang("ability.trace", pokemonName, it, effect.typelessData) }
"receiver", "powerofalchemy" -> optionalPokemonName?.let { battleLang("ability.receiver", it, effect.typelessData) } // Receiver and Power of Alchemy share the same text
else -> when (effect.id) {
"sturdy", "unnerve", "anticipation" -> battleLang("ability.${effect.id}", pokemonName) // Unique message
"airlock", "cloudnine" -> battleLang("ability.airlock") // Cloud Nine shares the same text as Air Lock
else -> null // Effect broadcasted by a succeeding instruction
}
}
battle.minorBattleActions[pokemon.uuid] = message
if (lang != null) {
battle.broadcastChatMessage(lang)
return@dispatch WaitDispatch(1F)
}
else return@dispatch GO
}
}
}
@@ -0,0 +1,62 @@
/*
* 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.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.CauserInstruction
import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.dispatch.WaitDispatch
import com.cobblemon.mod.common.util.battleLang
import net.minecraft.text.Text
/**
* Format:
* |-activate|POKEMON|EFFECT
*
* A miscellaneous effect has activated.This is triggered whenever an effect could not be better described by one of the other minor messages.
*/
class ActivateInstruction(val instructionSet: InstructionSet, val message: BattleMessage) : InterpreterInstruction, CauserInstruction {
override fun invoke(battle: PokemonBattle) {
val pokemon = message.getBattlePokemon(0, battle) ?: return
val pokemonName = pokemon.getName()
val sourceName = message.getSourceBattlePokemon(battle)?.getName() ?: Text.literal("UNKNOWN")
val effect = message.effectAt(1) ?: return
val extraEffect = message.effectAt(2)?.typelessData ?: Text.literal("UNKNOWN")
ShowdownInterpreter.broadcastOptionalAbility(battle, effect, pokemonName)
battle.dispatch{
ShowdownInterpreter.lastCauser[battle.battleId] = message
battle.minorBattleActions[pokemon.uuid] = message
val lang = when (effect.id) {
// Includes a 3rd argument being the magnitude level as a number
"magnitude" -> battleLang("activate.magnitude", message.argumentAt(2)?.toIntOrNull() ?: 1)
// Includes spited move and the PP it was reduced by
"spite", "eeriespell" -> battleLang("activate.spite", pokemonName, extraEffect, message.argumentAt(3)!!)
// Don't need additional lang, announced elsewhere
"toxicdebris", "shedskin" -> return@dispatch GO
// Add activation to each Pokemon's history
"destinybond" -> {
battle.activePokemon.mapNotNull { it.battlePokemon?.uuid }.forEach { battle.minorBattleActions[it] = message }
battleLang("activate.destinybond", pokemonName)
}
"focussash", "focusband" -> battleLang("activate.focusband", pokemonName, effect.typelessData)
"maxguard", "protect" -> battleLang("activate.protect", pokemonName)
"shadowforce", "hyperspacefury", "hyperspacehole" -> battleLang("activate.phantomforce", pokemonName)
else -> battleLang("activate.${effect.id}", pokemonName, sourceName, extraEffect)
}
battle.broadcastChatMessage(lang)
WaitDispatch(1F)
}
}
}
@@ -15,24 +15,34 @@ import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.GO
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.dispatch.WaitDispatch
import com.cobblemon.mod.common.net.messages.client.animation.PlayPoseableAnimationPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import com.cobblemon.mod.common.pokemon.evolution.progress.DamageTakenEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.RecoilEvolutionProgress
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.lang
import java.util.concurrent.CompletableFuture
import kotlin.math.roundToInt
import net.minecraft.text.Text
class DamageInstruction(
val instructionSet: InstructionSet,
val actor: BattleActor,
val publicMessage: BattleMessage,
val privateMessage: BattleMessage
) : InterpreterInstruction {
var future = CompletableFuture.completedFuture(Unit)
val battlePokemon = publicMessage.getBattlePokemon(0, actor.battle)
override fun invoke(battle: PokemonBattle) {
val battlePokemon = publicMessage.getBattlePokemon(0, battle) ?: return
if (privateMessage.optionalArgument("from")?.equals("recoil", true) == true) {
battlePokemon ?: return
val recoiling = privateMessage.optionalArgument("from")?.equals("recoil", true) == true
val lastCauser = instructionSet.getMostRecentCauser(comparedTo = this)
if (recoiling) {
battlePokemon.effectedPokemon.let { pokemon ->
if (RecoilEvolutionProgress.supports(pokemon)) {
val newPercentage = privateMessage.argumentAt(1)?.split("/")?.getOrNull(0)?.toIntOrNull() ?: 0
@@ -48,10 +58,34 @@ class DamageInstruction(
val newHealth = privateMessage.argumentAt(1)?.split(" ")?.get(0) ?: return
val effect = privateMessage.effect()
val pokemonName = battlePokemon.getName()
val sourceName = privateMessage.getSourceBattlePokemon(battle)?.getName() ?: Text.literal("UNKOWN")
ShowdownInterpreter.broadcastOptionalAbility(battle, effect, sourceName)
val sourceName = privateMessage.getSourceBattlePokemon(battle)?.getName() ?: Text.literal("UNKNOWN")
var causedFaint = newHealth == "0"
battle.dispatch {
if (lastCauser is MoveInstruction && recoiling) {
UntilDispatch { "recoil" !in lastCauser.holds }
} else {
GO
}
}
battle.dispatch {
if (!causedFaint) {
battlePokemon.entity?.let { pokemonEntity ->
val pkt = PlayPoseableAnimationPacket(pokemonEntity.id, setOf("recoil"), emptySet())
pkt.sendToPlayersAround(
x = pokemonEntity.x,
y = pokemonEntity.y,
z = pokemonEntity.z,
worldKey = pokemonEntity.world.registryKey,
distance = 50.0
)
}
}
ShowdownInterpreter.broadcastOptionalAbility(battle, effect, sourceName)
val newHealthRatio: Float
val remainingHealth = newHealth.split("/")[0].toInt()
@@ -70,8 +104,7 @@ class DamageInstruction(
battle.broadcastChatMessage(lang.red())
}
var causedFaint = false
if (newHealth == "0") {
if (causedFaint) {
newHealthRatio = 0F
battle.dispatch {
battlePokemon.effectedPokemon.currentHealth = 0
@@ -102,7 +135,9 @@ class DamageInstruction(
battle.minorBattleActions[battlePokemon.uuid] = privateMessage
// If they faint from this damage then don't bother waiting for the damage to be applied.
if (causedFaint) {
if (lastCauser is MoveInstruction && lastCauser.actionEffect != null && !causedFaint) {
UntilDispatch { lastCauser.future.isDone }
} else if (causedFaint) {
GO
} else {
WaitDispatch(1F)
@@ -0,0 +1,34 @@
/*
* 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.text.red
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
/**
* Format:
* |-miss|SOURCE|TARGET
*
* The move used by the SOURCE Pokémon missed (maybe absent) the TARGET Pokémon.
*/
class MissInstruction(val battle: PokemonBattle, val instructionSet: InstructionSet, val message: BattleMessage): InterpreterInstruction {
val target = message.getBattlePokemon(1, battle)
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo {
val pokemon = message.getBattlePokemon(0, battle) ?: return@dispatchGo
battle.broadcastChatMessage(battleLang("missed").red())
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
@@ -8,30 +8,55 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
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.molang.MoLangFunctions.addFunction
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.CauserInstruction
import com.cobblemon.mod.common.battles.dispatch.GO
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.util.battleLang
import java.util.concurrent.CompletableFuture
class MoveInstruction(
val instructionSet: InstructionSet,
val message: BattleMessage
) : InterpreterInstruction {
) : InterpreterInstruction, CauserInstruction {
val effect = message.effectAt(1) ?: Effect.pure("", "")
val move = Moves.getByNameOrDummy(effect.id)
val actionEffect = move.actionEffect
var future = CompletableFuture.completedFuture(Unit)
var holds = mutableSetOf<String>()
lateinit var userPokemon: BattlePokemon
var targetPokemon: BattlePokemon? = null
override fun invoke(battle: PokemonBattle) {
val userPokemon = message.getBattlePokemon(0, battle) ?: return
val targetPokemon = message.getBattlePokemon(2, battle)
val effect = message.effectAt(1) ?: return
userPokemon = message.getBattlePokemon(0, battle)!!
targetPokemon = message.getBattlePokemon(2, battle)
val targetPokemon = targetPokemon // So smart non-null casts can happen
val optionalEffect = message.effect()
val move = Moves.getByNameOrDummy(effect.id)
val pokemonName = userPokemon.getName()
ShowdownInterpreter.broadcastOptionalAbility(battle, optionalEffect, pokemonName)
battle.dispatch { UntilDispatch { instructionSet.getMostRecentInstruction<MoveInstruction>(this)?.future?.isDone != false } }
battle.dispatchGo {
battle.dispatch {
ShowdownInterpreter.lastCauser[battle.battleId] = message
userPokemon.effectedPokemon.let { pokemon ->
@@ -50,8 +75,50 @@ class MoveInstruction(
battleLang("used_move", pokemonName, move.displayName)
}
battle.broadcastChatMessage(lang)
battle.majorBattleActions[userPokemon.uuid] = message
val providers = mutableListOf<Any>(battle)
userPokemon.effectedPokemon.entity?.let { UsersProvider(it) }?.let(providers::add)
targetPokemon?.effectedPokemon?.entity?.let { TargetsProvider(it) }?.let(providers::add)
val runtime = MoLangRuntime().also {
battle.addQueryFunctions(it.environment.getQueryStruct()).addStandardFunctions()
}
actionEffect ?: return@dispatch GO
val context = ActionEffectContext(
actionEffect = actionEffect,
runtime = runtime,
providers = providers
)
val subsequentInstructions = instructionSet.findInstructionsCausedBy(this)
val missedTargets = subsequentInstructions.filterIsInstance<MissInstruction>().mapNotNull { it.target }
runtime.environment.getQueryStruct().addFunction("missed") { params ->
if (params.params.size == 0) {
return@addFunction DoubleValue(missedTargets.isNotEmpty())
} else {
val entityUUID = params.getString(0)
return@addFunction DoubleValue(missedTargets.any { it.entity?.uuidAsString == entityUUID })
}
}
val hurtTargets = subsequentInstructions.filterIsInstance<DamageInstruction>().mapNotNull { it.battlePokemon }
runtime.environment.getQueryStruct().addFunction("hurt") { params ->
if (params.params.size == 0) {
return@addFunction DoubleValue(hurtTargets.isNotEmpty())
} else {
val entityUUID = params.getString(0)
return@addFunction DoubleValue(hurtTargets.any { it.entity?.uuidAsString == entityUUID })
}
}
runtime.environment.getQueryStruct().addFunction("move") { move.struct }
this.future = actionEffect.run(context)
holds = context.holds // Reference so future things can check on this action effect's holds
future.thenApply { holds.clear() }
return@dispatch UntilDispatch { "effects" !in holds }
}
}
}
@@ -8,7 +8,10 @@
package com.cobblemon.mod.common.client.entity
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.entity.EntitySideDelegate
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.reactive.SettableObservable
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.scheduling.SchedulingTracker
@@ -37,6 +40,11 @@ class EmptyPokeBallClientDelegate : PokeBallPoseableState(), EntitySideDelegate<
this.currentEntity = entity
age = entity.age
initSubscriptions()
this.runtime.environment.getQueryStruct().addFunctions(mapOf(
"pokeball_type" to java.util.function.Function {
return@Function DoubleValue(currentEntity.pokeBall.name.toString())
}
))
}
override fun tick(entity: EmptyPokeBallEntity) {
@@ -8,9 +8,13 @@
package com.cobblemon.mod.common.client.entity
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.entity.PokemonSideDelegate
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.scheduling.SchedulingTracker
import com.cobblemon.mod.common.api.scheduling.afterOnClient
@@ -91,7 +95,7 @@ class PokemonClientDelegate : PoseableEntityState<PokemonEntity>(), PokemonSideD
if (isDying) {
val model = (currentModel ?: return) as PokemonPoseableModel
val animation = try {
model.getFaintAnimation(currentEntity, this)
model.getAnimation(this, "faint", runtime)
} catch (e: Exception) {
e.printStackTrace()
null
@@ -222,6 +226,27 @@ class PokemonClientDelegate : PoseableEntityState<PokemonEntity>(), PokemonSideD
override fun initialize(entity: PokemonEntity) {
this.currentEntity = entity
this.age = entity.age
this.runtime.environment.getQueryStruct().addFunctions(mapOf(
"in_battle" to java.util.function.Function {
return@Function DoubleValue(currentEntity.isBattling)
},
"shiny" to java.util.function.Function {
return@Function DoubleValue(currentEntity.pokemon.shiny)
},
"form" to java.util.function.Function {
return@Function StringValue(currentEntity.pokemon.form.name)
},
"width" to java.util.function.Function {
return@Function DoubleValue(currentEntity.boundingBox.xLength)
},
"height" to java.util.function.Function {
return@Function DoubleValue(currentEntity.boundingBox.yLength)
},
"weight" to java.util.function.Function {
return@Function DoubleValue(currentEntity.pokemon.species.weight.toDouble())
}
))
}
override fun tick(entity: PokemonEntity) {
@@ -248,12 +273,16 @@ class PokemonClientDelegate : PoseableEntityState<PokemonEntity>(), PokemonSideD
fun cry() {
val model = currentModel ?: return
if (model is PokemonPoseableModel) {
if (cryAnimation != null && cryAnimation in statefulAnimations) {
if (cryAnimation != null && (cryAnimation in statefulAnimations || cryAnimation == primaryAnimation)) {
return
}
val animation = model.cryAnimation(currentEntity, this) ?: return
statefulAnimations.add(animation)
if (animation is PrimaryAnimation) {
addPrimaryAnimation(animation)
} else {
statefulAnimations.add(animation)
}
cryAnimation = animation
}
}
@@ -138,7 +138,8 @@ abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Floa
pp = gimmickTemplate?.pp ?: moveTemplate.pp,
priority = gimmickTemplate?.priority ?: moveTemplate.priority,
critRatio = gimmickTemplate?.critRatio ?: moveTemplate.critRatio,
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances,
actionEffect = null
)
}
}
@@ -0,0 +1,32 @@
/*
* 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.client.net.animation
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.entity.Poseable
import com.cobblemon.mod.common.net.messages.client.animation.PlayPoseableAnimationPacket
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.resolve
import net.minecraft.client.MinecraftClient
object PlayPoseableAnimationHandler : ClientNetworkPacketHandler<PlayPoseableAnimationPacket> {
override fun handle(packet: PlayPoseableAnimationPacket, client: MinecraftClient) {
val entity = client.world?.getEntityById(packet.entityId) ?: return
if (entity is Poseable) {
val delegate = entity.delegate
if (delegate is PoseableEntityState<*>) {
for (expr in packet.expressions) {
delegate.runtime.resolve(expr.asExpression())
}
delegate.addFirstAnimation(packet.animation)
}
}
}
}
@@ -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.client.net.effect
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.entity.Poseable
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.resolve
import net.minecraft.client.MinecraftClient
object RunPosableMoLangHandler : ClientNetworkPacketHandler<RunPosableMoLangPacket> {
override fun handle(packet: RunPosableMoLangPacket, client: MinecraftClient) {
val entity = client.world?.getEntityById(packet.entityId) ?: return
if (entity is Poseable) {
val state = entity.delegate as? PoseableEntityState<*> ?: return
for (expression in packet.expressions.map { it.asExpression() }) {
state.runtime.resolve(expression)
}
}
}
}
@@ -0,0 +1,48 @@
/*
* 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.client.net.effect
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.particle.BedrockParticleEffectRepository
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.entity.Poseable
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import net.minecraft.client.MinecraftClient
import net.minecraft.entity.Entity
object SpawnSnowstormEntityParticleHandler : ClientNetworkPacketHandler<SpawnSnowstormEntityParticlePacket> {
override fun handle(packet: SpawnSnowstormEntityParticlePacket, client: MinecraftClient) {
val world = MinecraftClient.getInstance().world ?: return
val effect = BedrockParticleEffectRepository.getEffect(packet.effectId) ?: return
val entity = world.getEntityById(packet.entityId) as? Poseable ?: return
entity as Entity
val state = entity.delegate as PoseableEntityState<*>
val matrixWrapper = state.locatorStates[packet.locator] ?: state.locatorStates["root"]!!
val particleRuntime = MoLangRuntime().setup().setupClient()
particleRuntime.environment.getQueryStruct().addFunction("entity") { state.runtime.environment.getQueryStruct() }
val storm = ParticleStorm(
effect = effect,
matrixWrapper = matrixWrapper,
world = world,
runtime = particleRuntime,
sourceVelocity = { entity.velocity },
sourceAlive = { !entity.isRemoved },
sourceVisible = { !entity.isInvisible }
)
storm.spawn()
}
}
@@ -1,25 +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.client.net.sound
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.sound.PokemonCryPacket
import net.minecraft.client.MinecraftClient
object PokemonCryHandler : ClientNetworkPacketHandler<PokemonCryPacket> {
override fun handle(packet: PokemonCryPacket, client: MinecraftClient) {
val entity = client.world?.getEntityById(packet.entityId) ?: return
if (entity is PokemonEntity) {
val delegate = entity.delegate as PokemonClientDelegate
delegate.cry()
}
}
}
@@ -13,7 +13,6 @@ import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.ModAPI
import com.cobblemon.mod.common.api.snowstorm.ParticleMaterial
import com.cobblemon.mod.common.api.snowstorm.ParticleMaterials
import com.cobblemon.mod.common.api.snowstorm.UVDetails
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.util.resolveBoolean
@@ -24,7 +23,8 @@ import kotlin.math.abs
import net.minecraft.client.MinecraftClient
import net.minecraft.client.particle.Particle
import net.minecraft.client.particle.ParticleTextureSheet
import net.minecraft.client.particle.ParticleTextureSheet.*
import net.minecraft.client.particle.ParticleTextureSheet.NO_RENDER
import net.minecraft.client.particle.ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT
import net.minecraft.client.render.BufferBuilder
import net.minecraft.client.render.Camera
import net.minecraft.client.render.VertexConsumer
@@ -36,6 +36,6 @@ object BedrockAnimationReferenceFactory : AnimationReferenceFactory {
return model.bedrockStateful(
animationGroup = split[0],
animation = split[1]
).setPreventsIdle(if (split.size == 3) split[2].toBoolean() else JsonPokemonPoseableModel.StatefulAnimationAdapter.preventsIdleDefault)
)
}
}
@@ -8,16 +8,33 @@
package com.cobblemon.mod.common.client.render.models.blockbench
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.render.models.blockbench.animation.StatefulAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.StatelessAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.SimpleQuirk
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.resolveObject
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import net.minecraft.entity.Entity
import net.minecraft.util.math.Vec3d
class JsonPose<T : Entity>(model: PoseableEntityModel<T>, json: JsonObject) {
val poseName = json.get("poseName").asString
class JsonPoseTransition(val from: String, val to: String, val animation: ExpressionLike)
val runtime = MoLangRuntime().setup().setupClient().also {
it.environment.getQueryStruct().addFunctions(model.functions.functions)
}
val poseName = json.get("poseName")?.asString ?: "pose"
val poseTypes = (json.get("poseTypes")?.asJsonArray?.map { name ->
PoseType.values().find { it.name.lowercase() == name.asString.lowercase() }
?: throw IllegalArgumentException("Unknown pose type ${name.asString}")
@@ -47,18 +64,33 @@ class JsonPose<T : Entity>(model: PoseableEntityModel<T>, json: JsonObject) {
} else if (animString.startsWith("bedrock")) {
val split = animString.replace("bedrock(", "").replace(")", "").split(",").map(String::trim)
return@mapNotNull model.bedrock(animationGroup = split[0], animation = split[1])
} else {
try {
val expression = animString.asExpressionLike()
return@mapNotNull runtime.resolveObject(expression).obj as StatelessAnimation<T, *>
} catch (exception: Exception) {
null
}
}
return@mapNotNull null
}.toTypedArray()
val quirks = (json.get("quirks")?.asJsonArray ?: JsonArray()).map { json ->
if (json is JsonPrimitive) {
return@map json.asString.asExpressionLike().resolveObject(runtime).obj as SimpleQuirk<T>
}
json as JsonObject
val name = json.get("name").asString
val animations: (state: PoseableEntityState<T>) -> List<StatefulAnimation<T, *>> = { _ ->
(json.get("animations")?.asJsonArray ?: JsonArray()).map { animJson ->
val split =
animJson.asString.replace("bedrock(", "").replace(")", "").split(",").map(String::trim)
model.bedrockStateful(animationGroup = split[0], animation = split[1]).setPreventsIdle(false)
try {
val expr = animJson.asString.asExpressionLike()
runtime.resolveObject(expr).obj as StatefulAnimation<T, *>
} catch (e: Exception) {
val split =
animJson.asString.replace("bedrock(", "").replace(")", "").split(",").map(String::trim)
model.bedrockStateful(animationGroup = split[0], animation = split[1])
}
}
}
@@ -67,11 +99,26 @@ class JsonPose<T : Entity>(model: PoseableEntityModel<T>, json: JsonObject) {
val maxSeconds = json.get("maxSecondsBetweenOccurrences")?.asFloat ?: 30F
model.quirkMultiple(
name = name,
secondsBetweenOccurrences = minSeconds to maxSeconds,
condition = { true },
loopTimes = 1..loopTimes,
animations = animations
)
}
val animations = json.get("namedAnimations")?.takeIf { it is JsonObject }?.asJsonObject?.let {
val map = mutableMapOf<String, ExpressionLike>()
for ((key, value) in it.entrySet()) {
map[key] = value.asString.asExpressionLike()
}
map
} ?: mutableMapOf()
val transitions = json.get("transitions")?.takeIf { it is JsonArray }?.asJsonArray?.map {
it as JsonObject
val from = it.get("from").asString
val to = it.get("to").asString
val animation = it.get("animation").asString.asExpressionLike()
JsonPoseTransition(from, to, animation)
} ?: emptyList()
}
@@ -87,7 +87,14 @@ abstract class JsonPoseableEntityModel<T : Entity>(override val rootPart: Bone)
idleAnimations = pose.idleAnimations,
transformedParts = pose.transformedParts,
quirks = pose.quirks.toTypedArray()
)
).also {
it.transitions.putAll(
pose.transitions
.mapNotNull<JsonPose.JsonPoseTransition, Pair<String, (Pose<T, out ModelFrame>, Pose<T, out ModelFrame>) -> StatefulAnimation<T, ModelFrame>>> {
it.to to { _, _ -> it.animation.resolveObject(model.runtime).obj as StatefulAnimation<T, ModelFrame> }
}.toMap()
)
}
}
}
}
@@ -71,10 +71,6 @@ class LocatorAccess(
fun update(matrixStack: MatrixStack, state: MutableMap<String, MatrixWrapper>) {
matrixStack.push()
joint.transform(matrixStack)
matrixStack.push()
matrixStack.scale(1F, -1F, 1F)
state.getOrPut("root") { MatrixWrapper() }.updateMatrix(matrixStack.peek().positionMatrix)
matrixStack.pop()
for ((name, locator) in locators) {
matrixStack.push()
@@ -8,20 +8,28 @@
package com.cobblemon.mod.common.client.render.models.blockbench
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.struct.ArrayStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.scheduling.afterOnClient
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.client.render.MatrixWrapper
import com.cobblemon.mod.common.client.render.ModelLayer
import com.cobblemon.mod.common.client.render.layer.CobblemonRenderLayers
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PoseTransitionAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PrimaryAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.RotationFunctionStatelessAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.StatefulAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.StatelessAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.TranslationFunctionStatelessAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.*
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimationRepository
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockStatefulAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockStatelessAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPoseableModel
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
@@ -29,11 +37,15 @@ import com.cobblemon.mod.common.client.render.models.blockbench.quirk.ModelQuirk
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.SimpleQuirk
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunction
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.sineFunction
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.Poseable
import com.cobblemon.mod.common.entity.generic.GenericBedrockEntity
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.getDoubleOrNull
import com.cobblemon.mod.common.util.getStringOrNull
import net.minecraft.client.model.ModelPart
import net.minecraft.client.render.OverlayTexture
import net.minecraft.client.render.RenderLayer
@@ -82,6 +94,9 @@ abstract class PoseableEntityModel<T : Entity>(
@Transient
var currentState: PoseableEntityState<T>? = null
val animations = mutableMapOf<String, ExpressionLike>()
val transitions = mutableMapOf<String, ExpressionLike>()
/**
* A list of [ModelPartTransformation] that record the original
* This allows the original rotations to be reset.
@@ -91,12 +106,218 @@ abstract class PoseableEntityModel<T : Entity>(
val relevantParts = mutableListOf<ModelPart>()
val relevantPartsByName = mutableMapOf<String, ModelPart>()
@Transient
val functions = QueryStruct(hashMapOf())
.addFunction("bedrock_primary") { params ->
val group = params.getString(0)
val animation = params.getString(1)
val anim = bedrockStateful(group, animation)
val excludedLabels = mutableSetOf<String>()
var curve: WaveFunction = { t ->
if (t < 0.1) {
t * 10
} else if (t < 0.9) {
1F
} else {
1F
}
}
for (index in 2 until params.params.size) {
val param = params.get<MoValue>(index)
if (param is ObjectValue<*>) {
curve = param.obj as WaveFunction
continue
}
val label = params.getString(index) ?: continue
excludedLabels.add(label)
}
return@addFunction ObjectValue(PrimaryAnimation(animation = anim, excludedLabels = excludedLabels, curve = curve))
}
.addFunction("bedrock_stateful") { params ->
val group = params.getString(0)
val animation = params.getString(1)
val anim = bedrockStateful(group, animation)
return@addFunction ObjectValue(anim)
}
.addFunction("bedrock") { params ->
val group = params.getString(0)
val animation = params.getString(1)
val anim = bedrock(group, animation)
return@addFunction ObjectValue(anim)
}
.addFunction("look") { params ->
val boneName = params.getString(0)
val pitchMultiplier = params.getDoubleOrNull(1) ?: 1F
val yawMultiplier = params.getDoubleOrNull(2) ?: 1F
val maxPitch = params.getDoubleOrNull(3) ?: 70F
val minPitch = params.getDoubleOrNull(4) ?: -45F
val maxYaw = params.getDoubleOrNull(5) ?: 45F
ObjectValue(
SingleBoneLookAnimation<T>(
frame = this,
bone = getPart(boneName),
pitchMultiplier = pitchMultiplier.toFloat(),
yawMultiplier = yawMultiplier.toFloat(),
maxPitch = maxPitch.toFloat(),
minPitch = minPitch.toFloat(),
maxYaw = maxYaw.toFloat()
)
)
}
.addFunction("quadruped_walk") { params ->
val periodMultiplier = params.getDoubleOrNull(0) ?: 0.6662F
val amplitudeMultiplier = params.getDoubleOrNull(1) ?: 1.4F
val leftFrontLeftName = params.getStringOrNull(2) ?: "leg_front_left"
val leftFrontRightName = params.getStringOrNull(3) ?: "leg_front_right"
val leftBackLeftName = params.getStringOrNull(4) ?: "leg_back_left"
val leftBackRightName = params.getStringOrNull(5) ?: "leg_back_right"
ObjectValue(
QuadrupedWalkAnimation<T>(
frame = this,
periodMultiplier = periodMultiplier.toFloat(),
amplitudeMultiplier = amplitudeMultiplier.toFloat(),
legFrontLeft = this.getPart(leftFrontLeftName),
legFrontRight = this.getPart(leftFrontRightName),
legBackLeft = this.getPart(leftBackLeftName),
legBackRight = this.getPart(leftBackRightName)
)
)
}
.addFunction("biped_walk") { params ->
val periodMultiplier = params.getDoubleOrNull(0) ?: 0.6662F
val amplitudeMultiplier = params.getDoubleOrNull(1) ?: 1.4F
val leftLegName = params.getStringOrNull(2) ?: "leg_left"
val rightLegName = params.getStringOrNull(3) ?: "leg_right"
ObjectValue(
BipedWalkAnimation<T>(
frame = this,
periodMultiplier = periodMultiplier.toFloat(),
amplitudeMultiplier = amplitudeMultiplier.toFloat(),
leftLeg = this.getPart(leftLegName),
rightLeg = this.getPart(rightLegName)
)
)
}
.addFunction("bimanual_swing") { params ->
val swingPeriodMultiplier = params.getDoubleOrNull(0) ?: 0.6662F
val amplitudeMultiplier = params.getDoubleOrNull(1) ?: 1F
val leftArmName = params.getStringOrNull(2) ?: "arm_left"
val rightArmName = params.getStringOrNull(3) ?: "arm_right"
ObjectValue(
BimanualSwingAnimation<T>(
frame = this,
swingPeriodMultiplier = swingPeriodMultiplier.toFloat(),
amplitudeMultiplier = amplitudeMultiplier.toFloat(),
leftArm = this.getPart(leftArmName),
rightArm = this.getPart(rightArmName)
)
)
}
.addFunction("sine_wing_flap") { params ->
// verticalShift = -14F.toRadians(), period = 0.9F, amplitude = 0.9F
val amplitude = params.getDoubleOrNull(0) ?: 0.9F
val period = params.getDoubleOrNull(1) ?: 0.9F
val verticalShift = params.getDoubleOrNull(2) ?: 0F
val axis = params.getStringOrNull(3) ?: "y"
val axisIndex = when (axis) {
"x" -> ModelPartTransformation.X_AXIS
"y" -> ModelPartTransformation.Y_AXIS
"z" -> ModelPartTransformation.Z_AXIS
else -> ModelPartTransformation.Y_AXIS
}
val wingLeft = params.getStringOrNull(4) ?: "wing_left"
val wingRight = params.getStringOrNull(5) ?: "wing_right"
ObjectValue(
WingFlapIdleAnimation<T>(
frame = this,
rotation = sineFunction(verticalShift = verticalShift.toFloat(), period = period.toFloat(), amplitude = amplitude.toFloat()),
axis = axisIndex,
leftWing = this.getPart(wingLeft),
rightWing = this.getPart(wingRight)
)
)
}
.addFunction("bedrock_quirk") { params ->
val animationGroup = params.getString(0)
val animationNames = params.get<MoValue>(1)?.let { if (it is ArrayStruct) it.map.values.map { it.asString() } else listOf(it.asString()) } ?: listOf()
val minSeconds = params.getDoubleOrNull(2) ?: 8F
val maxSeconds = params.getDoubleOrNull(3) ?: 30F
val loopTimes = params.getDoubleOrNull(4)?.toInt() ?: 1
ObjectValue(
quirk(
secondsBetweenOccurrences = minSeconds.toFloat() to maxSeconds.toFloat(),
condition = { true },
loopTimes = 1..loopTimes,
animation = { bedrockStateful(animationGroup, animationNames.random()) }
)
)
}
@Transient
val runtime = MoLangRuntime().setup().setupClient().also { it.environment.getQueryStruct().addFunctions(functions.functions) }
/** Registers the different poses this model is capable of ahead of time. Should use [registerPose] religiously. */
abstract fun registerPoses()
/** Gets the [PoseableEntityState] for an entity. */
abstract fun getState(entity: T): PoseableEntityState<T>
fun getAnimation(state: PoseableEntityState<*>, name: String, runtime: MoLangRuntime): StatefulAnimation<T, *>? {
val poseAnimations = state.currentPose?.let(this::getPose)?.animations ?: mapOf()
val animation = resolveFromAnimationMap(poseAnimations, name, runtime)
?: resolveFromAnimationMap(animations, name, runtime)
?: if (name == "cry" && this is PokemonPoseableModel) {
cryAnimation.invoke(state.getEntity() as PokemonEntity, state as PoseableEntityState<PokemonEntity>) as? StatefulAnimation<T, *>
} else if (name == "faint" && this is PokemonPoseableModel) {
getFaintAnimation(state.getEntity() as PokemonEntity, state as PoseableEntityState<PokemonEntity>) as? StatefulAnimation<T, *>
} else {
try {
name.asExpressionLike().resolveObject(runtime).obj as StatefulAnimation<T, *>
} catch (exception: Exception) {
extractAnimation(name)
}
}
return animation
}
/**
* Animation group : animation name [: primary]
* e.g. "particle_dummy:animation.particle_dummy.dragon_claw_target:primary"
* e.g. "particle_dummy:animation.particle.dummy.stat_up
*/
fun extractAnimation(string: String): StatefulAnimation<T, *>? {
val group = string.substringBefore(":")
val animationName = string.substringAfter(":").substringBefore(":")
val isPrimary = string.endsWith(":primary")
if (animationName.isNotBlank() && animationName != string) {
val animation = BedrockAnimationRepository.tryGetAnimation(group, animationName) ?: return null
return if (isPrimary) {
PrimaryAnimation(BedrockStatefulAnimation(animation))
} else {
BedrockStatefulAnimation(animation)
}
} else {
return null
}
}
private fun resolveFromAnimationMap(map: Map<String, ExpressionLike>, name: String, runtime: MoLangRuntime): StatefulAnimation<T, *>? {
val animationExpression = map[name] ?: return null
return try {
animationExpression.resolveObject(runtime).obj as StatefulAnimation<T, *>
} catch (e: Exception) {
LOGGER.error("Failed to create animation by name $name, most likely something wrong in the MoLang")
e.printStackTrace()
null
}
}
fun withLayerContext(
buffer: VertexConsumerProvider,
state: PoseableEntityState<T>?,
@@ -145,6 +366,7 @@ abstract class PoseableEntityModel<T : Entity>(
poseType: PoseType,
condition: ((T) -> Boolean)? = null,
transformTicks: Int = 10,
animations: MutableMap<String, ExpressionLike> = mutableMapOf(),
onTransitionedInto: (PoseableEntityState<T>?) -> Unit = {},
idleAnimations: Array<StatelessAnimation<T, out F>> = emptyArray(),
transformedParts: Array<ModelPartTransformation> = emptyArray(),
@@ -156,6 +378,7 @@ abstract class PoseableEntityModel<T : Entity>(
condition,
onTransitionedInto,
transformTicks,
animations,
idleAnimations,
transformedParts,
quirks
@@ -169,6 +392,7 @@ abstract class PoseableEntityModel<T : Entity>(
poseTypes: Set<PoseType>,
condition: ((T) -> Boolean)? = null,
transformTicks: Int = 10,
animations: MutableMap<String, ExpressionLike> = mutableMapOf(),
onTransitionedInto: (PoseableEntityState<T>?) -> Unit = {},
idleAnimations: Array<StatelessAnimation<T, out F>> = emptyArray(),
transformedParts: Array<ModelPartTransformation> = emptyArray(),
@@ -180,6 +404,7 @@ abstract class PoseableEntityModel<T : Entity>(
condition,
onTransitionedInto,
transformTicks,
animations,
idleAnimations,
transformedParts,
quirks
@@ -193,6 +418,7 @@ abstract class PoseableEntityModel<T : Entity>(
poseType: PoseType,
condition: ((T) -> Boolean)? = null,
transformTicks: Int = 10,
animations: MutableMap<String, ExpressionLike> = mutableMapOf(),
onTransitionedInto: (PoseableEntityState<T>?) -> Unit = {},
idleAnimations: Array<StatelessAnimation<T, out F>> = emptyArray(),
transformedParts: Array<ModelPartTransformation> = emptyArray(),
@@ -204,6 +430,7 @@ abstract class PoseableEntityModel<T : Entity>(
condition,
onTransitionedInto,
transformTicks,
animations,
idleAnimations,
transformedParts,
quirks
@@ -436,7 +663,7 @@ abstract class PoseableEntityModel<T : Entity>(
val desirablePose = poses.values.firstOrNull {
(entityPoseType == null || entityPoseType in it.poseTypes) && it.isSuitable(entity)
}
?: Pose("none", setOf(PoseType.NONE), null, {}, 0, emptyArray(), emptyArray(), emptyArray())
?: Pose("none", setOf(PoseType.NONE), null, {}, 0, mutableMapOf(), emptyArray(), emptyArray(), emptyArray())
// If this condition matches then it just no longer fits this pose
if (pose != null && poseName != null) {
@@ -467,7 +694,8 @@ abstract class PoseableEntityModel<T : Entity>(
}
if (primaryAnimation != null) {
state.primaryOverridePortion = 1 - primaryAnimation.curve((state.animationSeconds - primaryAnimation.started) / primaryAnimation.duration)
val portion = (state.animationSeconds - primaryAnimation.started) / primaryAnimation.duration
state.primaryOverridePortion = 1 - primaryAnimation.curve(portion)
if (!primaryAnimation.run(entity, this, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch, 1 - state.primaryOverridePortion)) {
state.primaryAnimation = null
state.primaryOverridePortion = 1F
@@ -478,7 +706,7 @@ abstract class PoseableEntityModel<T : Entity>(
.filterNot { it.run(entity, this, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch, 1F) }
state.statefulAnimations.removeAll(removedStatefuls)
state.currentPose?.let { getPose(it) }
?.idleStateful(entity, this, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch, state.primaryOverridePortion)
?.idleStateful(entity, this, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch)
if (entity != null) {
updateLocators(state)
}
@@ -506,7 +734,7 @@ abstract class PoseableEntityModel<T : Entity>(
val desirablePoseType = desirablePose.poseTypes.first()
if (state.statefulAnimations.none { it.isTransform }) {
val transition = previousPose.transitions[desirablePose]
val transition = previousPose.transitions[desirablePose.poseName]
if (transition == null && previousPose.transformTicks > 0) {
val primaryAnimation = PrimaryAnimation(
PoseTransitionAnimation(
@@ -543,18 +771,21 @@ abstract class PoseableEntityModel<T : Entity>(
fun updateLocators(state: PoseableEntityState<T>) {
val entity = context.request(RenderContext.ENTITY) ?: return
val matrixStack = MatrixStack()
var scale = 1F
// We could improve this to be generalized for other entities
if (entity is PokemonEntity) {
matrixStack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180 - entity.bodyYaw))
matrixStack.push()
matrixStack.scale(-1F, -1F, 1F)
val scale = entity.pokemon.form.baseScale * entity.pokemon.scaleModifier * (entity.delegate as PokemonClientDelegate).entityScaleModifier
scale = entity.pokemon.form.baseScale * entity.pokemon.scaleModifier * (entity.delegate as PokemonClientDelegate).entityScaleModifier
matrixStack.scale(scale, scale, scale)
} else if (entity is EmptyPokeBallEntity) {
matrixStack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(entity.yaw))
matrixStack.push()
matrixStack.scale(1F, -1F, -1F)
matrixStack.scale(0.7F, 0.7F, 0.7F)
scale = 0.7F
matrixStack.scale(scale, scale, scale)
} else if (entity is GenericBedrockEntity) {
matrixStack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(entity.yaw))
matrixStack.push()
@@ -562,12 +793,27 @@ abstract class PoseableEntityModel<T : Entity>(
matrixStack.scale(1F, -1F, 1F)
}
matrixStack.push()
matrixStack.scale(1F, -1F, 1F)
val states = state.locatorStates
states.getOrPut("root") { MatrixWrapper() }.updateMatrix(matrixStack.peek().positionMatrix)
matrixStack.pop()
if (isForLivingEntityRenderer) {
// Standard living entity offset, only God knows why Mojang did this.
matrixStack.translate(0.0, -1.5, 0.0)
}
locatorAccess.update(matrixStack, state.locatorStates)
// If we have the entity, put in an approximation of the target locator. If the model has one defined,
// this will be overridden.
matrixStack.push()
matrixStack.translate(0.0, -entity.boundingBox.yLength / 2.0 / scale + 1.5F, -entity.width * 0.6 / scale)
matrixStack.scale(1F, -1F, 1F)
states.getOrPut("target") { MatrixWrapper() }.updateMatrix(matrixStack.peek().positionMatrix)
states.getOrPut("special_attack") { MatrixWrapper() }.updateMatrix(matrixStack.peek().positionMatrix)
matrixStack.pop()
locatorAccess.update(matrixStack, states)
}
fun ModelPart.translation(
@@ -606,21 +852,17 @@ abstract class PoseableEntityModel<T : Entity>(
fun bedrockStateful(
animationGroup: String,
animation: String,
animationPrefix: String = "animation.$animationGroup",
preventsIdleCheck: (T?, PoseableEntityState<T>, StatelessAnimation<T, *>) -> Boolean = { _, _, _ -> true }
) = BedrockStatefulAnimation(
animationPrefix: String = "animation.$animationGroup"
) = BedrockStatefulAnimation<T>(
BedrockAnimationRepository.getAnimation(animationGroup, "$animationPrefix.$animation"),
preventsIdleCheck
)
fun quirk(
name: String,
secondsBetweenOccurrences: Pair<Float, Float> = 8F to 30F,
loopTimes: IntRange = 1..1,
condition: (state: PoseableEntityState<T>) -> Boolean = { true },
animation: (state: PoseableEntityState<T>) -> StatefulAnimation<T, *>
) = SimpleQuirk(
name = name,
secondsBetweenOccurrences = secondsBetweenOccurrences,
loopTimes = loopTimes,
condition = condition,
@@ -628,16 +870,33 @@ abstract class PoseableEntityModel<T : Entity>(
)
fun quirkMultiple(
name: String,
secondsBetweenOccurrences: Pair<Float, Float> = 8F to 30F,
loopTimes: IntRange = 1..1,
condition: (state: PoseableEntityState<T>) -> Boolean = { true },
animations: (state: PoseableEntityState<T>) -> List<StatefulAnimation<T, *>>
) = SimpleQuirk(
name = name,
secondsBetweenOccurrences = secondsBetweenOccurrences,
loopTimes = loopTimes,
condition = condition,
animations = { animations(it) }
)
val dummyAnimation = object : StatefulAnimation<T, ModelFrame> {
override val isTransform = false
override val duration: Float = 1F
override fun run(
entity: T?,
model: PoseableEntityModel<T>,
state: PoseableEntityState<T>,
limbSwing: Float,
limbSwingAmount: Float,
ageInTicks: Float,
headYaw: Float,
headPitch: Float,
intensity: Float
): Boolean {
return false
}
}
}
@@ -9,20 +9,46 @@
package com.cobblemon.mod.common.client.render.models.blockbench
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.struct.VariableStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.MoValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunction
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.particle.BedrockParticleEffectRepository
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.client.entity.GenericBedrockClientDelegate
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.scheduling.Schedulable
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.render.MatrixWrapper
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PrimaryAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.StatefulAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.StatelessAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockParticleKeyframe
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockStatefulAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockStatelessAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.ModelQuirk
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.QuirkData
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunction
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunctions
import com.cobblemon.mod.common.entity.Poseable
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.resolve
import java.util.concurrent.ConcurrentLinkedQueue
import net.minecraft.client.MinecraftClient
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.client.world.ClientWorld
import net.minecraft.entity.Entity
import net.minecraft.sound.SoundCategory
import net.minecraft.sound.SoundEvent
import net.minecraft.util.math.Vec3d
/**
@@ -35,13 +61,90 @@ import net.minecraft.util.math.Vec3d
*/
abstract class PoseableEntityState<T : Entity> : Schedulable {
var currentModel: PoseableEntityModel<T>? = null
set(value) {
field = value
runtime.environment.getQueryStruct().addFunctions(value?.functions?.functions ?: hashMapOf())
}
var currentPose: String? = null
var primaryAnimation: PrimaryAnimation<T>? = null
val statefulAnimations: MutableList<StatefulAnimation<T, *>> = mutableListOf()
val quirks = mutableMapOf<ModelQuirk<T, *>, QuirkData<T>>()
val poseParticles = mutableListOf<BedrockParticleKeyframe>()
val runtime = MoLangRuntime().setup().also {
it.environment.structs["query"] = it.environment.structs["variable"]
val runtime = MoLangRuntime().setup().setupClient().also { runtime ->
val reusableAnimTime = DoubleValue(0.0) // This gets called 500 million times so use a mutable value for runtime
runtime.environment.getQueryStruct().addFunctions(mapOf(
"anim_time" to java.util.function.Function { return@Function reusableAnimTime.also { it.value = animationSeconds.toDouble() } },
"pose_type" to java.util.function.Function { return@Function StringValue((getEntity() as Poseable).getCurrentPoseType().name) },
"pose" to java.util.function.Function { _ -> return@Function StringValue(currentPose ?: "") },
"say" to java.util.function.Function { params -> MinecraftClient.getInstance().player?.sendMessage(params.getString(0).text()) ?: Unit },
"sound" to java.util.function.Function { params ->
val entity = getEntity() ?: return@Function Unit
if (params.get<MoValue>(0) !is StringValue) {
return@Function Unit
}
val soundEvent = SoundEvent.of(params.getString(0).asIdentifierDefaultingNamespace())
if (soundEvent != null) {
val volume = if (params.contains(1)) params.getDouble(1).toFloat() else 1F
val pitch = if (params.contains(2)) params.getDouble(2).toFloat() else 1F
MinecraftClient.getInstance().soundManager.play(
PositionedSoundInstance(soundEvent, SoundCategory.NEUTRAL, volume, pitch, entity.world.random, entity.x, entity.y, entity.z)
)
}
},
"play_animation" to java.util.function.Function { params ->
val animationParameter = params.get<MoValue>(0)
val animation = if (animationParameter is ObjectValue<*>) {
animationParameter.obj as BedrockStatefulAnimation<T>
} else {
currentModel?.getAnimation(this, animationParameter.asString(), runtime)
}
if (animation != null) {
if (animation is PrimaryAnimation<T>) {
addPrimaryAnimation(animation)
} else {
addStatefulAnimation(animation)
}
}
return@Function Unit
},
"particle" to java.util.function.Function { params ->
val particlesParam = params.get<MoValue>(0)
val particles = mutableListOf<String>()
when (particlesParam) {
is StringValue -> particles.add(particlesParam.value)
is VariableStruct -> particles.addAll(particlesParam.map.values.map { it.asString() })
else -> return@Function Unit
}
val effectIds = particles.map { it.asIdentifierDefaultingNamespace() }
for (effectId in effectIds) {
val locator = if (params.params.size > 1) params.getString(1) else "root"
val effect = BedrockParticleEffectRepository.getEffect(effectId) ?: run {
LOGGER.error("Unable to find a particle effect with id $effectId")
return@Function Unit
}
val entity = getEntity() ?: return@Function Unit
val world = entity.world as ClientWorld
val matrixWrapper = locatorStates[locator] ?: locatorStates["root"]!!
val particleRuntime = MoLangRuntime().setup().setupClient()
particleRuntime.environment.getQueryStruct().addFunction("entity") { runtime.environment.getQueryStruct() }
val storm = ParticleStorm(
effect = effect,
matrixWrapper = matrixWrapper,
world = world,
runtime = particleRuntime,
sourceVelocity = { entity.velocity },
sourceAlive = { !entity.isRemoved },
sourceVisible = { !entity.isInvisible }
)
storm.spawn()
}
}
))
}
val allStatefulAnimations: List<StatefulAnimation<T, *>> get() = statefulAnimations + quirks.flatMap { it.value.animations }
@@ -76,6 +179,23 @@ abstract class PoseableEntityState<T : Entity> : Schedulable {
val renderQueue = ConcurrentLinkedQueue<() -> Unit>()
/**
* Scans through the set of animations provided and begins playing the first one that is registered
* on the entity. The goal is to have most-specific animations first and more generic ones last, so
* where detailed animations exist they will be used and where they don't there is still a fallback.
*
* E.g. ['thunderbolt', 'electric', 'special']
*/
fun addFirstAnimation(animation: Set<String>) {
val model = currentModel ?: return
val animation = animation.firstNotNullOfOrNull { model.getAnimation(this, it, runtime) } ?: return
if (animation is PrimaryAnimation<T>) {
addPrimaryAnimation(animation)
} else {
addStatefulAnimation(animation)
}
}
fun isPosedIn(vararg poses: Pose<T, in ModelFrame>) = poses.any { it.poseName == currentPose }
fun isNotPosedIn(vararg poses: Pose<T, in ModelFrame>) = poses.none { it.poseName == currentPose }
@@ -155,7 +275,7 @@ abstract class PoseableEntityState<T : Entity> : Schedulable {
fun shouldIdleRun(idleAnimation: StatelessAnimation<T, *>, requiredIntensity: Float): Boolean {
val primaryAnimation = primaryAnimation
return if (primaryAnimation != null) {
!primaryAnimation.prevents(idleAnimation) && this.primaryOverridePortion >= requiredIntensity
!primaryAnimation.prevents(idleAnimation) || this.primaryOverridePortion > requiredIntensity
} else {
true
}
@@ -10,7 +10,12 @@ package com.cobblemon.mod.common.client.render.models.blockbench.animation
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityModel
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.addRotation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation.Companion.Y_AXIS
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation.Companion.Z_AXIS
import net.minecraft.entity.Entity
import net.minecraft.util.math.MathHelper
@@ -22,22 +27,36 @@ import net.minecraft.util.math.MathHelper
* @since December 21st, 2021
*/
class BimanualSwingAnimation<T : Entity>(
frame: BimanualFrame,
frame: ModelFrame,
/** The multiplier to apply to the cosine movement of the arms. The smaller this value, the quicker the arms move. */
val swingPeriodMultiplier: Float = 0.6662F,
/** The multiplier to apply to the swing of the entity. The larger this is, the further the arms move. */
val amplitudeMultiplier: Float = 1F
) : StatelessAnimation<T, BimanualFrame>(frame) {
override val targetFrame: Class<BimanualFrame> = BimanualFrame::class.java
val amplitudeMultiplier: Float = 1F,
val leftArm: Bone?,
val rightArm: Bone?
) : StatelessAnimation<T, ModelFrame>(frame) {
constructor(
frame: BimanualFrame,
swingPeriodMultiplier: Float = 0.6662F,
amplitudeMultiplier: Float = 1F
): this(
frame = frame,
swingPeriodMultiplier = swingPeriodMultiplier,
amplitudeMultiplier = amplitudeMultiplier,
leftArm = frame.leftArm,
rightArm = frame.rightArm
)
override val targetFrame: Class<ModelFrame> = ModelFrame::class.java
override fun setAngles(entity: T?, model: PoseableEntityModel<T>, state: PoseableEntityState<T>?, limbSwing: Float, limbSwingAmount: Float, ageInTicks: Float, headYaw: Float, headPitch: Float, intensity: Float) {
// Movement swing
frame.rightArm.yaw += MathHelper.cos(limbSwing * swingPeriodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity
frame.leftArm.yaw += MathHelper.cos(limbSwing * swingPeriodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity
rightArm?.addRotation(Y_AXIS, MathHelper.cos(limbSwing * swingPeriodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity)
leftArm?.addRotation(Y_AXIS, MathHelper.cos(limbSwing * swingPeriodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity)
// Idle sway
frame.rightArm.roll += 1.0f * (MathHelper.cos(ageInTicks * 0.09f) * 0.05f + 0.05f) * intensity
frame.rightArm.yaw += 1.0f * MathHelper.sin(ageInTicks * 0.067f) * 0.05f * intensity
frame.leftArm.roll += -1.0f * (MathHelper.cos(ageInTicks * 0.09f) * 0.05f + 0.05f) * intensity
frame.leftArm.yaw += -1.0f * MathHelper.sin(ageInTicks * 0.067f) * 0.05f * intensity
rightArm?.addRotation(Z_AXIS, 1.0f * (MathHelper.cos(ageInTicks * 0.09f) * 0.05f + 0.05f) * intensity)
rightArm?.addRotation(Y_AXIS, MathHelper.sin(ageInTicks * 0.067f) * 0.05f * intensity)
leftArm?.addRotation(Z_AXIS, -1.0f * (MathHelper.cos(ageInTicks * 0.09f) * 0.05f + 0.05f) * intensity)
leftArm?.addRotation(Y_AXIS, -1.0f * MathHelper.sin(ageInTicks * 0.067f) * 0.05f * intensity)
}
}
@@ -10,7 +10,11 @@ package com.cobblemon.mod.common.client.render.models.blockbench.animation
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityModel
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.addRotation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation.Companion.X_AXIS
import net.minecraft.entity.Entity
import net.minecraft.util.math.MathHelper
@@ -23,15 +27,29 @@ import net.minecraft.util.math.MathHelper
* @since December 21st, 2021
*/
class BipedWalkAnimation<T : Entity>(
frame: BipedFrame,
frame: ModelFrame,
/** The multiplier to apply to the cosine movement of the legs. The smaller this value, the quicker the legs move. */
val periodMultiplier: Float = 0.6662F,
/** The multiplier to apply to the stride of the entity. The larger this is, the further the legs move. */
val amplitudeMultiplier: Float = 1.4F
) : StatelessAnimation<T, BipedFrame>(frame) {
override val targetFrame: Class<BipedFrame> = BipedFrame::class.java
val amplitudeMultiplier: Float = 1.4F,
val leftLeg: Bone?,
val rightLeg: Bone?
) : StatelessAnimation<T, ModelFrame>(frame) {
constructor(
frame: BipedFrame,
periodMultiplier: Float = 0.6662F,
amplitudeMultiplier: Float = 1.4F
): this(
frame = frame,
periodMultiplier = periodMultiplier,
amplitudeMultiplier = amplitudeMultiplier,
leftLeg = frame.leftLeg,
rightLeg = frame.rightLeg
)
override val targetFrame: Class<ModelFrame> = ModelFrame::class.java
override fun setAngles(entity: T?, model: PoseableEntityModel<T>, state: PoseableEntityState<T>?, limbSwing: Float, limbSwingAmount: Float, ageInTicks: Float, headYaw: Float, headPitch: Float, intensity: Float) {
frame.rightLeg.pitch += MathHelper.cos(limbSwing * periodMultiplier + Math.PI.toFloat()) * limbSwingAmount * amplitudeMultiplier * intensity
frame.leftLeg.pitch += MathHelper.cos(limbSwing * periodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity
rightLeg?.addRotation(X_AXIS, MathHelper.cos(limbSwing * periodMultiplier + Math.PI.toFloat()) * limbSwingAmount * amplitudeMultiplier * intensity)
leftLeg?.addRotation(X_AXIS, MathHelper.cos(limbSwing * periodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity)
}
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.client.render.models.blockbench.animation
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityModel
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunction
import net.minecraft.entity.Entity
@@ -34,11 +35,12 @@ class PrimaryAnimation<T : Entity>(
}
},
val excludedLabels: Set<String> = emptySet(),
) {
val duration = animation.duration
override val isTransform: Boolean = false
): StatefulAnimation<T, ModelFrame> {
var started = -1F
override val duration: Float = animation.duration
fun run(
override fun run(
entity: T?,
model: PoseableEntityModel<T>,
state: PoseableEntityState<T>,
@@ -47,9 +49,9 @@ class PrimaryAnimation<T : Entity>(
ageInTicks: Float,
headYaw: Float,
headPitch: Float,
severity: Float
intensity: Float
): Boolean {
return animation.run(entity, model, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch, severity)
return animation.run(entity, model, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch, intensity)
}
fun prevents(idleAnimation: StatelessAnimation<T, *>) = idleAnimation.labels.intersect(excludedLabels).isEmpty() && "all" !in excludedLabels
@@ -10,7 +10,11 @@ package com.cobblemon.mod.common.client.render.models.blockbench.animation
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityModel
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.addRotation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.QuadrupedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation.Companion.X_AXIS
import net.minecraft.entity.Entity
import net.minecraft.util.math.MathHelper
@@ -23,17 +27,40 @@ import net.minecraft.util.math.MathHelper
* @since December 4th, 2021
*/
class QuadrupedWalkAnimation<T : Entity>(
frame: QuadrupedFrame,
frame: ModelFrame,
val legFrontLeft: Bone?,
val legFrontRight: Bone?,
val legBackLeft: Bone?,
val legBackRight: Bone?,
/** The multiplier to apply to the cosine movement of the legs. The smaller this value, the quicker the legs move. */
val periodMultiplier: Float = 0.6662F,
/** The multiplier to apply to the stride of the entity. The larger this is, the further the legs move. */
val amplitudeMultiplier: Float = 1.4F
) : StatelessAnimation<T, QuadrupedFrame>(frame) {
override val targetFrame: Class<QuadrupedFrame> = QuadrupedFrame::class.java
) : StatelessAnimation<T, ModelFrame>(frame) {
constructor(
frame: QuadrupedFrame,
periodMultiplier: Float = 0.6662F,
amplitudeMultiplier: Float = 1.4F
): this(
frame = frame,
legFrontLeft = frame.foreLeftLeg,
legFrontRight = frame.foreRightLeg,
legBackLeft = frame.hindLeftLeg,
legBackRight = frame.hindRightLeg,
periodMultiplier = periodMultiplier,
amplitudeMultiplier = amplitudeMultiplier
)
override val targetFrame: Class<ModelFrame> = ModelFrame::class.java
override fun setAngles(entity: T?, model: PoseableEntityModel<T>, state: PoseableEntityState<T>?, limbSwing: Float, limbSwingAmount: Float, ageInTicks: Float, headYaw: Float, headPitch: Float, intensity: Float) {
frame.hindRightLeg.pitch += MathHelper.cos(limbSwing * periodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity
frame.hindLeftLeg.pitch += MathHelper.cos(limbSwing * periodMultiplier + Math.PI.toFloat()) * limbSwingAmount * amplitudeMultiplier * intensity
frame.foreRightLeg.pitch += MathHelper.cos(limbSwing * periodMultiplier + Math.PI.toFloat()) * limbSwingAmount * amplitudeMultiplier * intensity
frame.foreLeftLeg.pitch += MathHelper.cos(limbSwing * periodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity
val hindRightLeg = legBackRight ?: return
val hindLeftLeg = legBackLeft ?: return
val foreRightLeg = legFrontRight ?: return
val foreLeftLeg = legFrontLeft ?: return
hindRightLeg.addRotation(X_AXIS, MathHelper.cos(limbSwing * periodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity)
hindLeftLeg.addRotation(X_AXIS, MathHelper.cos(limbSwing * periodMultiplier + Math.PI.toFloat()) * limbSwingAmount * amplitudeMultiplier * intensity)
foreRightLeg.addRotation(X_AXIS, MathHelper.cos(limbSwing * periodMultiplier + Math.PI.toFloat()) * limbSwingAmount * amplitudeMultiplier * intensity)
foreLeftLeg.addRotation(X_AXIS, MathHelper.cos(limbSwing * periodMultiplier) * limbSwingAmount * amplitudeMultiplier * intensity)
}
}
@@ -12,6 +12,8 @@ import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityMo
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.addRotation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation.Companion.X_AXIS
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation.Companion.Y_AXIS
import com.cobblemon.mod.common.util.math.geometry.toRadians
@@ -25,14 +27,39 @@ import net.minecraft.entity.Entity
* @author Hiroku
* @since December 5th, 2021
*/
class SingleBoneLookAnimation<T : Entity>(frame: HeadedFrame, val invertX: Boolean, val invertY: Boolean, val disableX: Boolean, val disableY: Boolean) : StatelessAnimation<T, HeadedFrame>(frame) {
override val targetFrame: Class<HeadedFrame> = HeadedFrame::class.java
class SingleBoneLookAnimation<T : Entity>(
frame: ModelFrame,
val bone: Bone?,
val pitchMultiplier: Float = 1F,
val yawMultiplier: Float = 1F,
val maxPitch: Float = 70F,
val minPitch: Float = 45F,
val maxYaw: Float = 45F,
) : StatelessAnimation<T, ModelFrame>(frame) {
constructor(
frame: HeadedFrame,
invertX: Boolean,
invertY: Boolean,
disableX: Boolean,
disableY: Boolean
): this(
frame = frame,
bone = frame.head,
pitchMultiplier = if (disableX) 0F else if (invertX) -1F else 1F,
yawMultiplier = if (disableY) 0F else if (invertY) -1F else 1F,
maxPitch = if (invertX) -45F else 45F,
minPitch = if (invertX) -70F else -45F,
maxYaw = if (invertY) -45F else 45F,
)
override val targetFrame: Class<ModelFrame> = ModelFrame::class.java
override var labels = setOf("look")
override fun setAngles(entity: T?, model: PoseableEntityModel<T>, state: PoseableEntityState<T>?, limbSwing: Float, limbSwingAmount: Float, ageInTicks: Float, headYaw: Float, headPitch: Float, intensity: Float) {
val pitch = (if (invertX) -1 else 1) * headPitch.coerceIn(-45F, 70F)
val yaw = (if (invertY) -1 else 1) * headYaw.coerceIn(-45F, 45F)
(if (!disableX) frame.head.addRotation(X_AXIS, pitch.toRadians() * intensity))
(if (!disableY) frame.head.addRotation(Y_AXIS, yaw.toRadians() * intensity))
val head = bone ?: return
val pitch = pitchMultiplier * headPitch.coerceIn(minPitch, maxPitch)
val yaw = yawMultiplier * headYaw.coerceIn(-maxYaw, maxYaw)
head.addRotation(X_AXIS, pitch.toRadians() * intensity)
head.addRotation(Y_AXIS, yaw.toRadians() * intensity)
}
}
@@ -12,19 +12,40 @@ import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityMo
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.addRotation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BiWingedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunction
import net.minecraft.entity.Entity
import net.minecraft.item.BoneMealItem
class WingFlapIdleAnimation<T : Entity>(
frame: BiWingedFrame,
val flapFunction: WaveFunction,
val timeVariable: (state: PoseableEntityState<T>?, limbSwing: Float, ageInTicks: Float) -> Float?,
frame: ModelFrame,
val leftWing: Bone?,
val rightWing: Bone?,
val rotation: WaveFunction,
val timeVariable: (state: PoseableEntityState<T>?, limbSwing: Float, ageInTicks: Float) -> Float? = { state, _, _ -> state?.animationSeconds ?: 0F },
val axis: Int
) : StatelessAnimation<T, BiWingedFrame>(frame) {
override val targetFrame = BiWingedFrame::class.java
) : StatelessAnimation<T, ModelFrame>(frame) {
constructor(
frame: BiWingedFrame,
flapFunction: WaveFunction,
timeVariable: (state: PoseableEntityState<T>?, limbSwing: Float, ageInTicks: Float) -> Float?,
axis: Int
): this(
frame = frame,
leftWing = frame.leftWing,
rightWing = frame.rightWing,
rotation = flapFunction,
timeVariable = timeVariable,
axis = axis
)
override val targetFrame = ModelFrame::class.java
override fun setAngles(entity: T?, model: PoseableEntityModel<T>, state: PoseableEntityState<T>?, limbSwing: Float, limbSwingAmount: Float, ageInTicks: Float, headYaw: Float, headPitch: Float, intensity: Float) {
val time = timeVariable(state, limbSwing, ageInTicks) ?: 0F
frame.leftWing.addRotation(axis, flapFunction(time) * intensity)
frame.rightWing.addRotation(axis, -flapFunction(time) * intensity)
val angle = rotation(time)
leftWing?.addRotation(axis, angle)
rightWing?.addRotation(axis, -angle)
}
}
@@ -15,6 +15,8 @@ import com.bedrockk.molang.runtime.MoScope
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.getQueryStruct
import com.cobblemon.mod.common.api.snowstorm.BedrockParticleEffect
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.particle.ParticleStorm
@@ -22,9 +24,11 @@ import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityMo
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.getString
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.resolve
import com.cobblemon.mod.common.util.resolveDouble
import java.util.SortedMap
import net.minecraft.client.MinecraftClient
@@ -76,10 +80,16 @@ class BedrockParticleKeyframe(
return
}
val particleRuntime = MoLangRuntime()
// Share the query struct from the entity so the particle can query entity properties
particleRuntime.environment.structs["query"] = state.runtime.environment.getQueryStruct()
val storm = ParticleStorm(
effect = effect,
matrixWrapper = matrixWrapper,
world = world,
runtime = particleRuntime,
sourceVelocity = { entity.velocity },
sourceAlive = { !entity.isRemoved && this in state.poseParticles },
sourceVisible = { !entity.isInvisible },
@@ -121,7 +131,7 @@ class BedrockInstructionKeyframe(
val expressions: List<Expression>
): BedrockEffectKeyframe(seconds) {
override fun <T : Entity> run(entity: T, state: PoseableEntityState<T>) {
expressions.forEach { expression -> BedrockAnimation.runInstruction(entity, state, expression) }
expressions.forEach { expression -> state.runtime.resolve(expression) }
}
}
@@ -132,64 +142,11 @@ data class BedrockAnimation(
val boneTimelines: Map<String, BedrockBoneTimeline>
) {
companion object {
val functionMappings = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
val sharedRuntime = MoLangRuntime().also {
it.environment.structs["query"] = it.environment.structs["variable"]
it.environment.structs["script"] = QueryStruct(functionMappings)
}
var context: InstructionContext? = null
inline fun <reified T : Entity> registerInstruction(name: String, crossinline function: (entity: T, state: PoseableEntityState<T>, params: MoParams) -> Any) {
functionMappings[name] = java.util.function.Function { args ->
val ctx = context ?: return@Function Unit
if (ctx.entity is T) {
val ret = function(ctx.entity, ctx.state as PoseableEntityState<T>, args)
if (ret is Unit) {
return@Function 0.0
} else {
return@Function ret
}
}
Unit
}
}
class InstructionContext(val entity: Entity, val state: PoseableEntityState<*>)
fun <T : Entity> runInstruction(entity: T, state: PoseableEntityState<T>, expression: Expression) {
val ctx = InstructionContext(entity, state)
context = ctx
expression.evaluate(MoScope(), sharedRuntime.environment)
context = null
}
init {
registerInstruction<Entity>("say") { entity, _, params ->
MinecraftClient.getInstance().player?.sendMessage(params.getString(0).text())
Unit
}
registerInstruction<Entity>("sound") { entity, _, params ->
// Means we don't need to setup a sound registry entry for every single thing
val soundEvent = SoundEvent.of(params.getString(0).asIdentifierDefaultingNamespace())
if (soundEvent != null) {
val volume = if (params.contains(1)) params.getDouble(1).toFloat() else 1F
val pitch = if (params.contains(2)) params.getDouble(2).toFloat() else 1F
MinecraftClient.getInstance().soundManager.play(
PositionedSoundInstance(soundEvent, SoundCategory.NEUTRAL, volume, pitch, entity.world.random, entity.x, entity.y, entity.z)
)
}
Unit
}
registerInstruction<Entity>("random") { entity, _, params ->
val options = mutableListOf<MoValue>()
var index = 0
while (params.contains(index)) {
options.add(params.get(index))
index++
}
return@registerInstruction options.random() // Can throw an exception if they specified no args. They'd be idiots though.
}
val zero = DoubleValue(0.0)
it.environment.getQueryStruct().addFunctions(mapOf(
"anim_time" to java.util.function.Function { return@Function zero }
))
}
}
@@ -55,6 +55,11 @@ object BedrockAnimationRepository {
LOGGER.info("Loaded $animationCount animations from ${animationGroups.size} animation groups")
}
fun tryGetAnimation(fileName: String, animationName: String): BedrockAnimation? {
val animationGroup = animationGroups[fileName] ?: return null
return animationGroup.animations[animationName]
}
fun getAnimation(fileName: String, animationName: String): BedrockAnimation {
val animationGroup = animationGroups[fileName]
?: throw IllegalArgumentException("Unknown animation group: $fileName")
@@ -24,13 +24,7 @@ import net.minecraft.entity.Entity
*/
open class BedrockStatefulAnimation<T : Entity>(
val animation: BedrockAnimation,
var preventsIdleCheck: (T?, PoseableEntityState<T>, StatelessAnimation<T, *>) -> Boolean
) : StatefulAnimation<T, ModelFrame> {
fun setPreventsIdle(preventsIdle: Boolean): BedrockStatefulAnimation<T> {
this.preventsIdleCheck = { _, _, _ -> preventsIdle }
return this
}
var startedSeconds = -1F
var isTransformAnimation = false
override val duration = animation.animationLength.toFloat()
@@ -39,10 +33,6 @@ open class BedrockStatefulAnimation<T : Entity>(
override val isTransform: Boolean
get() = isTransformAnimation
fun isTransformAnimation(value: Boolean) = this.also {
it.isTransformAnimation = value
}
fun andThen(action: (entity: T, PoseableEntityState<T>) -> Unit) = this.also {
it.afterAction = action
}
@@ -29,12 +29,12 @@ class FossilModel(root: Bone) : PoseableEntityModel<Entity>() {
var yGrowthPoint = 0F
var maxScale = 1F
var yTranslation = 0F // Offset inside the tank
var animations: Array<StatelessAnimation<Entity, out ModelFrame>> = emptyArray()
var tankAnimations: Array<StatelessAnimation<Entity, out ModelFrame>> = emptyArray()
var tankQuirks: Array<ModelQuirk<Entity, *>> = emptyArray()
override fun registerPoses() {
registerPose(
poseType = PoseType.SLEEP,
idleAnimations = animations,
idleAnimations = tankAnimations,
quirks = tankQuirks
)
}
@@ -51,9 +51,9 @@ class AncientPokeBallModel(root: ModelPart) : PokeBallModel(root), PokeBallFrame
transformTicks = 0
)
shut.transitions[open] = { _, _ -> bedrockStateful("ancient_poke_ball", "open").andThen { entity, state -> state.setPose(open.poseName) } }
open.transitions[shut] = { _, _ -> bedrockStateful("ancient_poke_ball", "shut").andThen { entity, state -> state.setPose(shut.poseName) } }
midair.transitions[open] = shut.transitions[open]!!
shut.transitions[open.poseName] = { _, _ -> bedrockStateful("ancient_poke_ball", "open").andThen { entity, state -> state.setPose(open.poseName) } }
open.transitions[shut.poseName] = { _, _ -> bedrockStateful("ancient_poke_ball", "shut").andThen { entity, state -> state.setPose(shut.poseName) } }
midair.transitions[open.poseName] = shut.transitions[open.poseName]!!
}
}
@@ -52,9 +52,9 @@ open class PokeBallModel(root: ModelPart) : PoseableEntityModel<EmptyPokeBallEnt
transformTicks = 0
)
shut.transitions[open] = { _, _ -> bedrockStateful("poke_ball", "open").andThen { entity, state -> state.setPose(open.poseName) } }
open.transitions[shut] = { _, _ -> bedrockStateful("poke_ball", "shut").andThen { entity, state -> state.setPose(shut.poseName) } }
midair.transitions[open] = shut.transitions[open]!!
shut.transitions[open.poseName] = { _, _ -> bedrockStateful("poke_ball", "open") }
open.transitions[shut.poseName] = { _, _ -> bedrockStateful("poke_ball", "shut") }
midair.transitions[open.poseName] = shut.transitions[open.poseName]!!
}
}
@@ -8,19 +8,27 @@
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.client.render.models.blockbench.JsonPose
import com.cobblemon.mod.common.client.render.models.blockbench.JsonPoseableEntityModel
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.animation.StatefulAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockStatefulAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.ModelFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
import com.cobblemon.mod.common.util.adapters.Vec3dAdapter
import com.google.gson.*
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
@@ -53,6 +61,7 @@ class JsonPokemonPoseableModel(override val rootPart: Bone) : PokemonPoseableMod
).type,
JsonPoseableEntityModel.StatefulAnimationAdapter { JsonPokemonPoseableModelAdapter.model!! }
)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.registerTypeAdapter(Pose::class.java, PoseAdapter)
.registerTypeAdapter(JsonPokemonPoseableModel::class.java, JsonPokemonPoseableModelAdapter)
.create()
@@ -93,12 +102,11 @@ class JsonPokemonPoseableModel(override val rootPart: Bone) : PokemonPoseableMod
get() = _profileTranslation ?: Vec3d(0.0, 0.0, 0.0)
val faint: Supplier<StatefulAnimation<PokemonEntity, ModelFrame>>? = null
val cry: Supplier<StatefulAnimation<PokemonEntity, ModelFrame>>? = null
override fun getFaintAnimation(pokemonEntity: PokemonEntity, state: PoseableEntityState<PokemonEntity>) = faint?.get()
override val cryAnimation = CryProvider { _, _ -> cry?.get()?.also { if (it is BedrockStatefulAnimation) it.setPreventsIdle(false) } }
override val cryAnimation = CryProvider { _, _ -> cry?.get() }
object JsonModelExclusion: ExclusionStrategy {
override fun shouldSkipField(f: FieldAttributes): Boolean {
@@ -116,7 +124,6 @@ class JsonPokemonPoseableModel(override val rootPart: Bone) : PokemonPoseableMod
}
object StatefulAnimationAdapter : JsonDeserializer<Supplier<StatefulAnimation<PokemonEntity, ModelFrame>>> {
var preventsIdleDefault = true
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): Supplier<StatefulAnimation<PokemonEntity, ModelFrame>> {
json as JsonPrimitive
val animString = json.asString
@@ -135,6 +142,7 @@ class JsonPokemonPoseableModel(override val rootPart: Bone) : PokemonPoseableMod
}
}
}
object PoseAdapter : JsonDeserializer<Pose<PokemonEntity, ModelFrame>> {
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): Pose<PokemonEntity, ModelFrame> {
val model = JsonPokemonPoseableModelAdapter.model!!
@@ -142,6 +150,7 @@ class JsonPokemonPoseableModel(override val rootPart: Bone) : PokemonPoseableMod
val pose = JsonPose(model, obj)
val conditionsList = mutableListOf<(PokemonEntity) -> Boolean>()
val mustBeInBattle = json.get("isBattle")?.asBoolean
if (mustBeInBattle != null) {
conditionsList.add { mustBeInBattle == it.isBattling }
@@ -153,73 +162,23 @@ class JsonPokemonPoseableModel(override val rootPart: Bone) : PokemonPoseableMod
val poseCondition: ((PokemonEntity) -> Boolean)? = if (conditionsList.isEmpty()) null else conditionsList.reduce { acc, function -> { acc(it) && function(it) } }
val transformedParts = obj.get("transformedParts")?.asJsonArray?.map {
it as JsonObject
val partName = it.get("part").asString
val part = model.getPart(partName)
val transformation = ModelPartTransformation(part)
val rotation = it.get("rotation")?.asJsonArray?.let { Vec3d(it[0].asDouble, it[1].asDouble, it[2].asDouble) } ?: Vec3d.ZERO
val position = it.get("position")?.asJsonArray?.let { Vec3d(it[0].asDouble, it[1].asDouble, it[2].asDouble) } ?: Vec3d.ZERO
val isVisible = it.get("isVisible")?.asBoolean ?: true
return@map transformation.withPosition(position.x, position.y, position.z).withRotationDegrees(rotation.x, rotation.y, rotation.z).withVisibility(isVisible)
}?.toTypedArray() ?: arrayOf()
val idleAnimations = (obj.get("animations")?.asJsonArray ?: JsonArray()).asJsonArray.mapNotNull {
val animString = it.asString
if (animString == "look") {
return@mapNotNull JsonPokemonPoseableModelAdapter.model!!.singleBoneLook<PokemonEntity>()
} else {
val anim = animString.substringBefore("(")
if(ANIMATION_FACTORIES.contains(anim)) {
return@mapNotNull ANIMATION_FACTORIES[anim]?.stateless(model, animString)
}
}
return@mapNotNull null
}.toTypedArray()
val quirks = (obj.get("quirks")?.asJsonArray ?: JsonArray()).map { json ->
json as JsonObject
val name = json.get("name").asString
val animations: (state: PoseableEntityState<PokemonEntity>) -> List<StatefulAnimation<PokemonEntity, *>> = { _ ->
(json.get("animations")?.asJsonArray ?: JsonArray()).map { animJson ->
val animString = animJson.asString
val anim = animString.substringBefore("(")
StatefulAnimationAdapter.preventsIdleDefault = false
val animation = if(ANIMATION_FACTORIES.contains(anim)) {
ANIMATION_FACTORIES[anim]?.stateful(model, animString)
} else {
null
}
StatefulAnimationAdapter.preventsIdleDefault = true
animation
}.filterNotNull()
}
val loopTimes = json.get("loopTimes")?.asInt ?: 1
val minSeconds = json.get("minSecondsBetweenOccurrences")?.asFloat ?: 8F
val maxSeconds = json.get("maxSecondsBetweenOccurrences")?.asFloat ?: 30F
model.quirkMultiple(
name = name,
secondsBetweenOccurrences = minSeconds to maxSeconds,
condition = { true },
loopTimes = 1..loopTimes,
animations = animations
)
}
return Pose(
poseName = pose.poseName,
poseTypes = pose.poseTypes.toSet(),
condition = poseCondition,
animations = pose.animations,
transformTicks = pose.transformTicks,
idleAnimations = pose.idleAnimations,
transformedParts = pose.transformedParts,
quirks = pose.quirks.toTypedArray()
)
).also {
it.transitions.putAll(
pose.transitions
.mapNotNull<JsonPose.JsonPoseTransition, Pair<String, (Pose<PokemonEntity, out ModelFrame>, Pose<PokemonEntity, out ModelFrame>) -> StatefulAnimation<PokemonEntity, ModelFrame>>> {
it.to to { _, _ -> it.animation.resolveObject(model.runtime).obj as StatefulAnimation<PokemonEntity, ModelFrame> }
}.toMap()
)
}
}
}
}
@@ -87,11 +87,6 @@ abstract class PokemonPoseableModel : PoseableEntityModel<PokemonEntity>() {
}
open val cryAnimation: CryProvider = CryProvider { _, _ -> null }
//
// open fun getCryAnimation(
// pokemonEntity: PokemonEntity,
// state: PoseableEntityState<PokemonEntity>
// ): StatefulAnimation<PokemonEntity, ModelFrame>? = null
override fun setupEntityTypeContext(entity: PokemonEntity?) {
entity?.let {
@@ -33,7 +33,7 @@ class AerodactylModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame {
lateinit var fly: PokemonPose
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("aerodactyl", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("aerodactyl", "blink")}
sleep = registerPose(
poseType = PoseType.SLEEP,
idleAnimations = arrayOf(bedrock("aerodactyl", "sleep"))
@@ -41,7 +41,7 @@ class AlakazamModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Bipe
lateinit var walk: PokemonPose
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("alakazam", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("alakazam", "blink")}
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + UI_POSES,
@@ -51,10 +51,10 @@ class ArbokModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame {
val tail4WaveSegment = WaveSegment(modelPart = tail4, length = 11F)
val tail5WaveSegment = WaveSegment(modelPart = tail5, length = 11F)
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("arbok", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("arbok", "cry") }
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("arbok", "blink").setPreventsIdle(false) }
val blink = quirk { bedrockStateful("arbok", "blink") }
// TODO tongue_flick
val wave = WaveAnimation<PokemonEntity>(
@@ -38,7 +38,7 @@ class BeedrillModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, BiWi
lateinit var standing: PokemonPose
lateinit var walk: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("beedrill", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("beedrill", "cry") }
override fun registerPoses() {
sleep = registerPose(
@@ -1,109 +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.client.render.models.blockbench.pokemon.gen1
import com.cobblemon.mod.common.client.render.models.blockbench.EarJoint
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.RangeOfMotion
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.EaredFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPose
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPoseableModel
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.math.geometry.toRadians
import net.minecraft.client.model.ModelPart
import net.minecraft.util.math.Vec3d
class BlastoiseModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, BipedFrame, BimanualFrame, EaredFrame {
override val rootPart = root.registerChildWithAllChildren("blastoise")
override val head = getPart("head_ai")
override val rightArm = getPart("arm_right")
override val leftArm = getPart("arm_left")
override val rightLeg = getPart("leg_right")
override val leftLeg = getPart("leg_left")
private val rightEar = getPart("ear_right")
private val leftEar = getPart("ear_left")
override val leftEarJoint = EarJoint(leftEar, ModelPartTransformation.Z_AXIS, RangeOfMotion(50F.toRadians(), 0F))
override val rightEarJoint = EarJoint(rightEar, ModelPartTransformation.Z_AXIS, RangeOfMotion((-50F).toRadians(), 0F))
override val portraitScale = 1.7F
override val portraitTranslation = Vec3d(-0.5, 1.4, 0.0)
override val profileScale = 0.63F
override val profileTranslation = Vec3d(0.0, 0.8, 0.0)
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("blastoise", "cry").setPreventsIdle(false) }
lateinit var sleep: PokemonPose
lateinit var standing: PokemonPose
lateinit var swimIdle: PokemonPose
lateinit var swimMove: PokemonPose
lateinit var walk: PokemonPose
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("blastoise", "blink").setPreventsIdle(false)}
sleep = registerPose(
poseType = PoseType.SLEEP,
transformTicks = 10,
idleAnimations = arrayOf(bedrock("blastoise", "sleep"))
)
standing = registerPose(
poseName = "standing",
poseTypes = setOf(PoseType.NONE, PoseType.PROFILE, PoseType.PORTRAIT, PoseType.STAND),
transformTicks = 10,
quirks = arrayOf(blink),
idleAnimations = arrayOf(
singleBoneLook(),
bedrock("blastoise", "ground_idle")
)
)
swimIdle = registerPose(
poseName = "swim_idle",
poseTypes = setOf(PoseType.FLOAT),
transformTicks = 10,
quirks = arrayOf(blink),
idleAnimations = arrayOf(
bedrock("blastoise", "water_idle"),
singleBoneLook()
)
)
swimMove = registerPose(
poseName = "swim_move",
poseTypes = setOf(PoseType.SWIM),
transformTicks = 10,
quirks = arrayOf(blink),
idleAnimations = arrayOf(
singleBoneLook(),
bedrock("blastoise", "water_swim")
)
)
walk = registerPose(
poseType = PoseType.WALK,
transformTicks = 10,
quirks = arrayOf(blink),
idleAnimations = arrayOf(
singleBoneLook(),
bedrock("blastoise", "ground_walk")
)
)
}
override fun getFaintAnimation(
pokemonEntity: PokemonEntity,
state: PoseableEntityState<PokemonEntity>
) = if (state.isPosedIn(standing, walk, sleep)) bedrockStateful("blastoise", "faint") else null
}
@@ -36,10 +36,10 @@ class BulbasaurModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Qua
lateinit var standing: PokemonPose
lateinit var walk: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("bulbasaur", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("bulbasaur", "cry") }
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("bulbasaur", "blink").setPreventsIdle(false) }
val blink = quirk { bedrockStateful("bulbasaur", "blink") }
sleep = registerPose(
poseType = PoseType.SLEEP,
@@ -35,7 +35,7 @@ class ButterfreeModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Bi
lateinit var sleep: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("butterfree", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("butterfree", "cry") }
override fun registerPoses() {
sleep = registerPose(
@@ -34,10 +34,10 @@ class CaterpieModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame {
lateinit var standing: PokemonPose
lateinit var walk: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("caterpie", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("caterpie", "cry") }
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("caterpie", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("caterpie", "blink")}
sleep = registerPose(
poseType = PoseType.SLEEP,
transformTicks = 10,
@@ -33,7 +33,7 @@ class ChanseyModel(root: ModelPart) : PokemonPoseableModel(), BipedFrame {
lateinit var walk: PokemonPose
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("chansey", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("chansey", "blink")}
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + UI_POSES,
@@ -8,13 +8,11 @@
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen1
import com.cobblemon.mod.common.client.render.models.blockbench.PoseableEntityState
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BiWingedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BimanualFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.HeadedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPose
import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.PokemonPoseableModel
import com.cobblemon.mod.common.client.render.models.blockbench.pose.ModelPartTransformation.Companion.Y_AXIS
@@ -22,9 +20,10 @@ import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.PoseType.Companion.MOVING_POSES
import com.cobblemon.mod.common.entity.PoseType.Companion.STATIONARY_POSES
import com.cobblemon.mod.common.entity.PoseType.Companion.UI_POSES
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpressionLike
import net.minecraft.client.model.ModelPart
import net.minecraft.util.math.Vec3d
class CharizardModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, BipedFrame, BimanualFrame, BiWingedFrame {
override val rootPart = root.registerChildWithAllChildren("charizard")
override val head = getPart("head_ai")
@@ -47,13 +46,20 @@ class CharizardModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Bip
lateinit var flyIdle: PokemonPose
lateinit var fly: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("charizard", "cry").setPreventsIdle(false) }
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("charizard", "blink").setPreventsIdle(false)}
animations["physical"] = "q.bedrock_primary('charizard', 'physical', 'look', q.curve('symmetrical_wide'))".asExpressionLike()
animations["special"] = "q.bedrock_primary('charizard', 'special', 'look', q.curve('symmetrical_wide'))".asExpressionLike()
animations["status"] = "q.bedrock_primary('charizard', 'status', q.curve('symmetrical_wide'))".asExpressionLike()
animations["recoil"] = "q.bedrock_stateful('charizard', 'recoil')".asExpressionLike()
animations["cry"] = "q.bedrock_stateful('charizard', 'cry')".asExpressionLike()
val faint = "q.bedrock_primary('charizard', 'faint', q.curve('one'))".asExpressionLike()
val blink = quirk { bedrockStateful("charizard", "blink") }
sleep = registerPose(
poseType = PoseType.SLEEP,
quirks = arrayOf(blink),
animations = mutableMapOf("faint" to faint),
idleAnimations = arrayOf(bedrock("charizard", "sleep"))
)
@@ -61,16 +67,30 @@ class CharizardModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Bip
poseName = "standing",
poseTypes = STATIONARY_POSES - PoseType.HOVER + UI_POSES,
quirks = arrayOf(blink),
condition = { !it.isBattling },
animations = mutableMapOf("faint" to faint),
idleAnimations = arrayOf(
singleBoneLook(),
bedrock("charizard", "ground_idle")
)
)
registerPose(
poseTypes = setOf(PoseType.STAND),
poseName = "battle_standing",
condition = { it.isBattling },
animations = mutableMapOf("faint" to faint),
idleAnimations = arrayOf(
singleBoneLook(),
bedrock("charizard", "battle_idle")
)
)
walk = registerPose(
poseName = "walk",
poseTypes = MOVING_POSES - PoseType.FLY,
quirks = arrayOf(blink),
animations = mutableMapOf("faint" to faint),
idleAnimations = arrayOf(
singleBoneLook(),
bedrock("charizard", "ground_idle"),
@@ -100,9 +120,4 @@ class CharizardModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Bip
transformedParts = arrayOf(rootPart.createTransformation().addPosition(Y_AXIS, 6F))
)
}
override fun getFaintAnimation(
pokemonEntity: PokemonEntity,
state: PoseableEntityState<PokemonEntity>
) = if (state.isPosedIn(standing, walk, sleep)) bedrockStateful("charizard", "faint") else null
}
@@ -41,7 +41,7 @@ class CharmanderModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Bi
lateinit var standing: PokemonPose
lateinit var walk: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("charmander", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("charmander", "cry") }
override fun registerPoses() {
sleep = registerPose(
@@ -49,7 +49,7 @@ class CharmanderModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Bi
idleAnimations = arrayOf(bedrock("charmander", "sleep"))
)
val blink = quirk("blink") { bedrockStateful("charmander", "blink").setPreventsIdle(false) }
val blink = quirk { bedrockStateful("charmander", "blink") }
standing = registerPose(
poseName = "standing",
@@ -39,10 +39,10 @@ class CharmeleonModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Bi
lateinit var standing: PokemonPose
lateinit var walk: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("charmeleon", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("charmeleon", "cry") }
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("charmeleon", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("charmeleon", "blink")}
sleep = registerPose(
poseType = PoseType.SLEEP,
transformTicks = 10,
@@ -33,10 +33,10 @@ class ClefableModel(root: ModelPart) : PokemonPoseableModel() {
lateinit var standing: PokemonPose
lateinit var walk: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("clefable", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("clefable", "cry") }
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("clefable", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("clefable", "blink")}
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + UI_POSES,
@@ -33,10 +33,10 @@ class ClefairyModel(root: ModelPart) : PokemonPoseableModel() {
lateinit var standing: PokemonPose
lateinit var walk: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("clefairy", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("clefairy", "cry") }
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("clefairy", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("clefairy", "blink")}
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + UI_POSES,
@@ -32,7 +32,7 @@ class DiglettModel(root: ModelPart) : PokemonPoseableModel() {
lateinit var walk: PokemonPose
lateinit var sleep: PokemonPose
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("diglett", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("diglett", "blink")}
stand = registerPose(
poseName = "stand",
poseTypes = STATIONARY_POSES + UI_POSES,
@@ -33,10 +33,10 @@ class DittoModel(root: ModelPart) : PokemonPoseableModel() {
lateinit var walk: PokemonPose
lateinit var sleep: PokemonPose
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("ditto", "cry").setPreventsIdle(false) }
override val cryAnimation = CryProvider { _, _ -> bedrockStateful("ditto", "cry") }
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("ditto", "blink").setPreventsIdle(false) }
val blink = quirk { bedrockStateful("ditto", "blink") }
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + UI_POSES,
@@ -32,9 +32,9 @@ class DodrioModel (root: ModelPart) : PokemonPoseableModel() {
lateinit var battleidle: PokemonPose
override fun registerPoses() {
val blink1 = quirk("blink1") { bedrockStateful("dodrio", "blink1").setPreventsIdle(false) }
val blink2 = quirk("blink2") { bedrockStateful("dodrio", "blink2").setPreventsIdle(false) }
val blink3 = quirk("blink3") { bedrockStateful("dodrio", "blink3").setPreventsIdle(false) }
val blink1 = quirk { bedrockStateful("dodrio", "blink1") }
val blink2 = quirk { bedrockStateful("dodrio", "blink2") }
val blink3 = quirk { bedrockStateful("dodrio", "blink3") }
sleep = registerPose(
poseType = PoseType.SLEEP,
idleAnimations = arrayOf(bedrock("dodrio", "sleep"))
@@ -32,10 +32,10 @@ class DoduoModel (root: ModelPart) : PokemonPoseableModel() {
lateinit var battleidle: PokemonPose
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("doduo", "blink1").setPreventsIdle(false) }
val blink2 = quirk("blink2") { bedrockStateful("doduo", "blink2").setPreventsIdle(false) }
val bite = quirk("bite") { bedrockStateful("doduo", "bite_quirk1").setPreventsIdle(false) }
val bite2 = quirk("bite2") { bedrockStateful("doduo", "bite_quirk2").setPreventsIdle(false) }
val blink = quirk { bedrockStateful("doduo", "blink1") }
val blink2 = quirk { bedrockStateful("doduo", "blink2") }
val bite = quirk { bedrockStateful("doduo", "bite_quirk1") }
val bite2 = quirk { bedrockStateful("doduo", "bite_quirk2") }
sleep = registerPose(
poseType = PoseType.SLEEP,
@@ -32,7 +32,7 @@ class DragonairModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame {
// TODO dragonair swish
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("dragonair", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("dragonair", "blink")}
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + MOVING_POSES + UI_POSES,
@@ -47,7 +47,7 @@ class DragoniteModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, BiW
lateinit var fly: PokemonPose
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("dragonite", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("dragonite", "blink")}
standing = registerPose(
poseName = "standing",
poseTypes = UI_POSES + PoseType.STAND,
@@ -30,7 +30,7 @@ class DratiniModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame {
lateinit var standing: PokemonPose
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("dratini", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("dratini", "blink")}
standing = registerPose(
poseName = "standing",
poseTypes = STATIONARY_POSES + MOVING_POSES + UI_POSES,
@@ -38,7 +38,7 @@ class DrowzeeModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Biped
lateinit var battleidle: PokemonPose
override fun registerPoses() {
val blink = quirk("blink") { bedrockStateful("drowzee", "blink").setPreventsIdle(false)}
val blink = quirk { bedrockStateful("drowzee", "blink")}
sleep = registerPose(
poseType = PoseType.SLEEP,
idleAnimations = arrayOf(bedrock("drowzee", "sleep"))

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