Compare commits

...
Author SHA1 Message Date
Plastered_Crab 86d48df9fe Render Rotom drone camera to start orbiting around + try to avoid collisions using raycast point cloud 2025-02-28 16:59:03 -08:00
Flarkinater 3401ba9cbb Fix drone animation 2025-02-15 21:48:29 -05:00
Flarkinater dac9ccb461 Revert debug change 2025-02-15 21:47:59 -05:00
Flarkinater 191a62f693 Attempt to debug the animation issue 2025-02-15 16:50:52 -05:00
Flarkinater d8d410e539 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into stadium-block 2025-02-15 15:37:37 -05:00
Flarkinater b7c6e1ea77 Attempt to make the drone animated (WIP) 2025-02-15 15:35:48 -05:00
Hiroku 0af4588a14 README change [skip-ci] 2025-02-15 20:26:29 +00:00
Flarkinater 186812d6fd Refactor - move most drone assets into bedrock/drone 2025-02-14 12:23:27 -05:00
Polymeta df9dc8c029 Merge branch 'feat/capture-qol-feature-parity' into 'main'
Critical capture feature parity

See merge request cable-mc/cobblemon!1516
2025-02-14 16:35:16 +00:00
Ina 51dc606246 Critical capture feature parity 2025-02-14 16:35:15 +00:00
Polymeta 790f8347f8 Merge branch 'flying_shoulder_mons' into 'main'
Fix flying shoulder mons

See merge request cable-mc/cobblemon!1530
2025-02-13 14:55:01 +00:00
JPAK 09de901b35 Fix flying shoulder mons 2025-02-13 08:14:46 -06:00
Flarkinater 1c0bdc766a Fix upside-down drone 2025-02-12 14:55:41 -05:00
Flarkinater c697bcd2e9 Render the drone in-game 2025-02-12 01:17:42 -05:00
Polymeta dcf321da8f Merge branch 'pokemon-follow-mouse' into 'main'
Add functionality for Pokémon to follow the mouse cursor

Closes #282

See merge request cable-mc/cobblemon!1361
2025-02-11 13:46:48 +00:00
lion c32bd4d014 Add functionality for Pokémon to follow the mouse cursor 2025-02-11 13:46:47 +00:00
Polymeta ab4e2f2540 Merge branch 'test/shoulder_caching' into 'main'
Touch up shoulder rendering cache management

Closes #1367

See merge request cable-mc/cobblemon!1499
2025-02-11 11:37:21 +00:00
aeroeng15 741bfb3316 Touch up shoulder rendering cache management 2025-02-11 11:37:20 +00:00
Flarkinater 9d6ff01d80 Add Rotom drone model/texture/animation placeholders 2025-02-10 20:20:24 -05:00
Flarkinater 1691853a19 Remove placeholder model, we'll just use some "dev art" for now 2025-02-10 20:20:12 -05:00
Flarkinater 4e9a4e1e5a Update deprecated methods, imports, and formatting 2025-02-10 20:20:03 -05:00
Flarkinater 6450255a0b Rebase old stadium camera test branch 2025-02-10 20:19:13 -05:00
43 changed files with 1111 additions and 100 deletions

No files matched your search

+4
View File
@@ -5,6 +5,7 @@
- Added an in-game configuration screen, allowing all settings from `main.json` to be edited directly in-game.
- Added `/cobblemonconfig reload` command to reload `main.json` configuration. **Note:** Some settings require a server restart to take effect; use this command cautiously.
- Added `blacklisted_items_to_hold` and `whitelisted_items_to_hold` tags to allow for controlling which items players can give to their Pokémon. If the whitelist is empty, it will consider all item as allowed (unless they are in the blacklist).
- Pokémon now follow the mouse cursor on the Summary screen, with an option to disable this in the settings.
### Changes
- Renamed `chargeGainedPerTick` config to `secondsToChargeHealingMachine`.
@@ -19,6 +20,7 @@
- Fixed players getting disconnected when sidemods update a Pokémon's teratype
- Fixed fling not using Item Names for minecraft held items that substitute Pokémon items
- Fixed evolutions sometimes preventing players from logging in to servers
- Fixed rendering of shoulder-mounted Pokémon desyncing between clients
### Developer
- A finished battle now has winners and losers set inside of `PokemonBattle` instead of them always being empty.
@@ -30,6 +32,8 @@
- Renamed Cobblemon's creative tabs to start with "Cobblemon: " to distinguish Cobblemon's tabs from tabs for other mods.
- Various items now have a rarity value.
### Changes
- When using the `cobblemon` or `generation_9` capture calculators a critical capture with a single shake will always play for successful captures when you've already registered the Pokémon as caught in your Pokédex.
### MoLang & Datapacks
- The following usages for item predicates can now use item conditions like advancements do, you can learn about them in the [Minecraft wiki](https://minecraft.wiki/w/Advancement_definition#minecraft:filled_bucket)
- The `requiredContext` for an item interaction evolution
+1 -1
View File
@@ -1,6 +1,6 @@
# Cobblemon
An open-source Pokémon mod for Minecraft Java Edition, written in Kotlin for Fabric and Forge.
An open-source Pokémon mod for Minecraft Java Edition, written in Kotlin for Fabric and NeoForge.
## Getting started
@@ -36,6 +36,9 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
@JvmField
val PC: BlockEntityType<PCBlockEntity> = this.create("pc", BlockEntityType.Builder.of(::PCBlockEntity, CobblemonBlocks.PC).build(null))
@JvmField
val STADIUM: BlockEntityType<StadiumBlockEntity> = this.create("stadium_terminal", BlockEntityType.Builder.of(::StadiumBlockEntity, CobblemonBlocks.STADIUM).build(null))
@JvmField
val LECTERN: BlockEntityType<LecternBlockEntity> = this.create("lectern", BlockEntityType.Builder.of(::LecternBlockEntity, CobblemonBlocks.LECTERN).build(null))
@@ -354,6 +354,9 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val PC = create("pc", PCBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).noOcclusion().lightLevel { if ((it.getValue(PCBlock.ON) as Boolean) && (it.getValue(PCBlock.PART) == PCBlock.PCPart.TOP)) 10 else 0 }))
@JvmField
val STADIUM = create("stadium_terminal", StadiumBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F)))
@JvmField
val LECTERN = create("lectern", LecternBlock(BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).instrument(NoteBlockInstrument.BASS).sound(SoundType.WOOD).strength(2.5F).ignitedByLava()))
@@ -272,6 +272,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val PC = blockItem("pc", CobblemonBlocks.PC)
@JvmField
val STADIUM = blockItem("stadium_terminal", CobblemonBlocks.STADIUM)
@JvmField
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
@JvmField
val DISPLAY_CASE = blockItem("display_case", CobblemonBlocks.DISPLAY_CASE)
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.api.events.pokemon
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.api.pokemon.stats.EvSource
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.pokemon.Pokemon
/**
* Fired during EV mutation.
@@ -25,6 +26,11 @@ interface EvGainedEvent {
*/
val stat: Stat
/**
* @see EvSource.pokemon
*/
val pokemon: Pokemon get() = this.source.pokemon
/**
* The [EvSource] that fired this event.
*/
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.pokeball.catching.calculators
import com.cobblemon.mod.common.api.pokeball.catching.CaptureContext
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.pokedex
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.LivingEntity
/**
* An influencer on capture results based on Pokedex data.
*/
interface PokedexStatusCaptureInfluencer {
/**
* Attempts to influence the [context] of a capture.
*
* @param thrower The [LivingEntity] that threw the Pokeball
* @param target The [PokemonEntity] attempting to capture.
* @param context The calculated [CaptureContext].
* @return The final [CaptureContext].
*/
fun influence(thrower: LivingEntity, target: PokemonEntity, context: CaptureContext): CaptureContext {
if (!context.isSuccessfulCapture) {
return context
}
val player = thrower as? ServerPlayer ?: return context
val pokedex = player.pokedex()
val speciesId = target.pokemon.species.resourceIdentifier
val formName = target.pokemon.form.name
val hasCaughtStatus = pokedex.getOrCreateSpeciesRecord(speciesId)
.getOrCreateFormRecord(formName)
.knowledge == PokedexEntryProgress.CAUGHT
// In modern games when the catch is a success and the Pokémon has already been registered to the Pokédex
// the catch becomes a critical capture and only requires one shake
return if (hasCaughtStatus) {
CaptureContext(
numberOfShakes = 1,
isSuccessfulCapture = true,
isCriticalCapture = true
)
} else context
}
}
@@ -167,10 +167,10 @@ open class PlayerPartyStore(
}
// Shoulder validation code
if (player.shoulderEntityLeft.isPokemonEntity() && !validateShoulder(player.shoulderEntityLeft, true)) {
if (player.shoulderEntityLeft.isPokemonEntity() && !validateShoulder(player, true)) {
player.respawnEntityOnShoulder(player.shoulderEntityLeft)
}
if (player.shoulderEntityRight.isPokemonEntity() && !validateShoulder(player.shoulderEntityRight, false)) {
if (player.shoulderEntityRight.isPokemonEntity() && !validateShoulder(player, false)) {
player.respawnEntityOnShoulder(player.shoulderEntityRight)
}
@@ -182,11 +182,14 @@ open class PlayerPartyStore(
}
}
fun validateShoulder(shoulderEntity: CompoundTag, isLeft: Boolean): Boolean {
private fun validateShoulder(player: ServerPlayer, isLeft: Boolean): Boolean {
val shoulderEntity = if(isLeft) player.shoulderEntityLeft else player.shoulderEntityRight
val pokemon = find { it.uuid == shoulderEntity.getCompound("Pokemon").getUUID(DataKeys.POKEMON_UUID) }
if (pokemon == null || (pokemon.state as? ShoulderedState)?.isLeftShoulder != isLeft) {
// No longer valid if (in order): not in party, not the correct shoulder, no longer shoulder mountable
if (pokemon == null || (pokemon.state as? ShoulderedState)?.isLeftShoulder != isLeft || !pokemon.form.shoulderMountable) {
return false
}
player.updateShoulderNbt(pokemon)
return true
}
@@ -0,0 +1,113 @@
/*
* 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.block
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.block.entity.StadiumBlockEntity
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.BaseEntityBlock
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.HorizontalDirectionalBlock
import net.minecraft.world.level.block.Mirror
import net.minecraft.world.level.block.RenderShape
import net.minecraft.world.level.block.Rotation
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.phys.BlockHitResult
class StadiumBlock(properties: Properties): BaseEntityBlock(properties) {
companion object {
val CODEC = simpleCodec(::StadiumBlock)
}
init {
registerDefaultState(stateDefinition.any().setValue(HorizontalDirectionalBlock.FACING, Direction.NORTH))
}
override fun codec() = CODEC
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity {
return StadiumBlockEntity(pos, state)
}
override fun getStateForPlacement(blockPlaceContext: BlockPlaceContext): BlockState {
return this.defaultBlockState().setValue(HorizontalDirectionalBlock.FACING, blockPlaceContext.horizontalDirection)
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(HorizontalDirectionalBlock.FACING)
}
override fun rotate(blockState: BlockState, rotation: Rotation): BlockState {
return blockState.setValue(
HorizontalDirectionalBlock.FACING, rotation.rotate(blockState.getValue(
HorizontalDirectionalBlock.FACING)))
}
override fun mirror(blockState: BlockState, mirror: Mirror): BlockState {
return blockState.rotate(mirror.getRotation(blockState.getValue(HorizontalDirectionalBlock.FACING)))
}
override fun onRemove(state: BlockState, world: Level, pos: BlockPos, newState: BlockState, moved: Boolean) {
if (!state.`is`(newState.block)) super.onRemove(state, world, pos, newState, moved)
}
//TODO: override getTicker
override fun getRenderShape(state: BlockState) = RenderShape.MODEL
override fun useWithoutItem(
blockState: BlockState,
world: Level,
blockPos: BlockPos,
player: Player,
blockHitResult: BlockHitResult
): InteractionResult {
if (player !is ServerPlayer) return InteractionResult.SUCCESS
val blockEntity = world.getBlockEntity(blockPos)
if (blockEntity !is StadiumBlockEntity) return InteractionResult.SUCCESS
// Toggle isInUse state
blockEntity.isInUse = !blockEntity.isInUse
// debug
val activateMessage = if (blockEntity.isInUse) "Stadium Activated!" else "Stadium Deactivated!"
player.sendSystemMessage(text(activateMessage).red())
if (blockEntity.isInUse) {
// Generate flight path when activated
blockEntity.generateFlightPath(world, radiusX = 5.0, radiusZ = 3.0, height = 10.0, steps = 20)
} else {
// Clear path when deactivated
blockEntity.pathPoints.clear()
}
val message = "Stadium Pos: " + blockEntity.blockPos +
"\nPlayer 1 Pos: " + blockEntity.getPlayer1Pos() +
"\nPoke 1 Pos: " + blockEntity.getPoke1Pos() +
"\nPlayer 2 Pos: " + blockEntity.getPlayer2Pos() +
"\nPoke 2 Pos: " + blockEntity.getPoke2Pos()
player.sendSystemMessage(text(message).red())
blockEntity.setChanged()
world.sendBlockUpdated(blockPos, blockState, blockState, Block.UPDATE_CLIENTS)
return InteractionResult.SUCCESS
}
}
@@ -0,0 +1,132 @@
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.client.render.models.blockbench.drone.DroneState
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.network.protocol.Packet
import net.minecraft.network.protocol.game.ClientGamePacketListener
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket
import net.minecraft.network.protocol.game.ClientboundPlayerLookAtPacket
import net.minecraft.network.protocol.game.ClientboundSetCarriedItemPacket
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.phys.Vec3
class StadiumBlockEntity(
pos: BlockPos,
blockState: BlockState
): BlockEntity(
CobblemonBlockEntities.STADIUM,
pos,
blockState
) {
var isInUse = false
val droneState = DroneState()
val pathPoints: MutableList<Vec3> = mutableListOf()
fun generateFlightPath(world: Level, radiusX: Double, radiusZ: Double, height: Double, steps: Int) {
pathPoints.clear()
for (i in 0 until steps) {
val angle = (i.toDouble() / steps) * Math.PI * 2
val xOffset = radiusX * Math.cos(angle)
val zOffset = radiusZ * Math.sin(angle)
val basePos = Vec3(blockPos.x + xOffset, blockPos.y + height, blockPos.z + zOffset)
val adjustedPos = getValidPoint(world, basePos)
pathPoints.add(adjustedPos)
}
}
private fun getValidPoint(world: Level, start: Vec3): Vec3 {
val maxHeight = 40 // Max height for drone path
val stepHeight = 1.0 // Step size for raycast
var adjustedHeight = start.y
for (h in 0..maxHeight step stepHeight.toInt()) {
val checkPos = BlockPos(start.x.toInt(), start.y.toInt() - h, start.z.toInt()) // Convert to BlockPos
if (!world.getBlockState(checkPos).isAir) { // Check if block is solid
return Vec3(start.x, start.y - h - 1, start.z) // Adjust position
}
}
return start // No obstruction found
}
fun getPlayer1Pos(): BlockPos {
return blockPos.north(10)
}
fun getPlayer2Pos(): BlockPos {
return blockPos.south(10)
}
fun getPoke1Pos(): BlockPos {
return blockPos.north(5)
}
fun getPoke2Pos(): BlockPos {
return blockPos.south(5)
}
/**
* Saves data to NBT.
*/
override fun saveAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.saveAdditional(tag, registries)
// Save the isInUse state
tag.putBoolean("IsInUse", isInUse)
// Save path points
val pathTag = ListTag()
for (point in pathPoints) {
val pointTag = CompoundTag()
pointTag.putDouble("x", point.x)
pointTag.putDouble("y", point.y)
pointTag.putDouble("z", point.z)
pathTag.add(pointTag)
}
tag.put("PathPoints", pathTag)
}
/**
* Loads data from NBT.
*/
override fun loadAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
super.loadAdditional(tag, registries)
// Load the isInUse state
isInUse = tag.getBoolean("IsInUse")
// Load path points
pathPoints.clear()
val pathTag = tag.getList("PathPoints", 10) // 10 is the ID for CompoundTag
for (i in 0 until pathTag.size) {
val pointTag = pathTag.getCompound(i)
val point = Vec3(pointTag.getDouble("x"), pointTag.getDouble("y"), pointTag.getDouble("z"))
pathPoints.add(point)
}
}
override fun getUpdatePacket(): Packet<ClientGamePacketListener>? {
//println("getUpdatePacket called for NestBlockEntity at $blockPos with egg: $eggComponent")
return ClientboundBlockEntityDataPacket.create(this)
}
override fun getUpdateTag(registryLookup: HolderLookup.Provider): CompoundTag {
val tag = saveWithoutMetadata(registryLookup)
//println("getUpdateTag called for NestBlockEntity at $blockPos with tag: $tag")
return tag
}
}
@@ -25,13 +25,7 @@ import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.render.DeferredRenderer
import com.cobblemon.mod.common.client.render.block.BerryBlockRenderer
import com.cobblemon.mod.common.client.render.block.DisplayCaseRenderer
import com.cobblemon.mod.common.client.render.block.FossilAnalyzerRenderer
import com.cobblemon.mod.common.client.render.block.GildedChestBlockRenderer
import com.cobblemon.mod.common.client.render.block.HealingMachineRenderer
import com.cobblemon.mod.common.client.render.block.LecternBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.RestorationTankRenderer
import com.cobblemon.mod.common.client.render.block.*
import com.cobblemon.mod.common.client.render.boat.CobblemonBoatRenderer
import com.cobblemon.mod.common.client.render.entity.PokeBobberEntityRenderer
import com.cobblemon.mod.common.client.render.generic.GenericBedrockRenderer
@@ -256,7 +250,8 @@ object CobblemonClient {
CobblemonBlocks.SKY_TUMBLESTONE_CLUSTER,
CobblemonBlocks.GIMMIGHOUL_CHEST,
CobblemonBlocks.DISPLAY_CASE,
CobblemonBlocks.LECTERN
CobblemonBlocks.LECTERN,
CobblemonBlocks.STADIUM
)
this.createBoatModelLayers()
@@ -290,6 +285,7 @@ object CobblemonClient {
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.GILDED_CHEST, ::GildedChestBlockRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.DISPLAY_CASE, ::DisplayCaseRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.LECTERN, ::LecternBlockEntityRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.STADIUM, ::StadiumBlockRenderer)
}
private fun registerEntityRenderers() {
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.client.render.models.blockbench.repository.Rende
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.util.math.toEulerXYZDegrees
import com.cobblemon.mod.common.util.toHex
import com.mojang.blaze3d.platform.Lighting
import com.mojang.blaze3d.systems.RenderSystem
@@ -25,8 +26,10 @@ import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import org.joml.Quaternionf
import org.joml.Vector3f
import kotlin.math.atan
fun drawProfilePokemon(
renderablePokemon: RenderablePokemon,
@@ -41,7 +44,9 @@ fun drawProfilePokemon(
r: Float = 1F,
g: Float = 1F,
b: Float = 1F,
a: Float = 1F
a: Float = 1F,
headYaw: Float = 0f,
headPitch: Float = 0f,
) = drawProfilePokemon(
species = renderablePokemon.species.resourceIdentifier,
matrixStack = matrixStack,
@@ -56,6 +61,8 @@ fun drawProfilePokemon(
g = g,
b = b,
a = a,
headYaw = headYaw,
headPitch = headPitch,
)
fun drawProfilePokemon(
@@ -71,7 +78,9 @@ fun drawProfilePokemon(
r: Float = 1F,
g: Float = 1F,
b: Float = 1F,
a: Float = 1F
a: Float = 1F,
headYaw: Float = 0f,
headPitch: Float = 0f,
) {
RenderSystem.applyModelViewMatrix()
matrixStack.scale(scale, scale, -scale)
@@ -100,7 +109,7 @@ fun drawProfilePokemon(
state.setPoseToFirstSuitable(poseType)
state.updatePartialTicks(partialTicks)
model.applyAnimations(null, state, 0F, 0F, 0F, 0F, 0F)
if (applyProfileTransform) {
matrixStack.translate(
model.profileTranslation.x,
@@ -114,6 +123,9 @@ fun drawProfilePokemon(
}
}
matrixStack.mulPose(rotation)
model.applyAnimations(null, state, 0F, 0F, 0F, headYaw, headPitch)
Lighting.setupForEntityInInventory()
val entityRenderDispatcher = Minecraft.getInstance().entityRenderDispatcher
rotation.conjugate()
@@ -140,3 +152,29 @@ fun drawProfilePokemon(
}
}
const val HEAD_YAW_FACTOR = 40f
const val HEAD_PITCH_FACTOR = 20f
const val SMOOTHING_FACTOR = 0.05f
fun calculateHeadYawAndPitch(
xCoordinate: Float,
yCoordinate: Float,
rotation: Quaternionf,
mouseX: Int,
mouseY: Int,
currentYaw: Float,
currentPitch: Float,
resetToCenter: Boolean,
): Pair<Float, Float> {
val horizontalAngle = atan(((xCoordinate - mouseX) / 40.0f).toDouble()).toFloat()
val verticalAngle = atan(((yCoordinate - mouseY) / 40.0f).toDouble()).toFloat()
val eulerXYZRotationDegrees = rotation.toEulerXYZDegrees()
val targetYaw = if (!resetToCenter) horizontalAngle * HEAD_YAW_FACTOR - eulerXYZRotationDegrees.y else 0f
val targetPitch = if (!resetToCenter) -verticalAngle * HEAD_PITCH_FACTOR - eulerXYZRotationDegrees.x else 0f
val newYaw = Mth.rotLerp(SMOOTHING_FACTOR, currentYaw, targetYaw)
val newPitch = Mth.rotLerp(SMOOTHING_FACTOR, currentPitch, targetPitch)
return Pair(newYaw, newPitch)
}
@@ -161,7 +161,7 @@ class StarterSelectionScreen(private val categories: List<RenderableStarterCateg
pX = x + 119, pY = height / 2 + 84,
pWidth = StarterRoundabout.MODEL_WIDTH, pHeight = StarterRoundabout.MODEL_HEIGHT,
pokemon = currentPokemon,
rotationVector = this.modelWidget.rotVec
rotationVector = this.modelWidget.rotationVector
)
starterRoundaboutLeft = StarterRoundabout(
@@ -169,7 +169,7 @@ class StarterSelectionScreen(private val categories: List<RenderableStarterCateg
pWidth = StarterRoundabout.MODEL_WIDTH, pHeight = StarterRoundabout.MODEL_HEIGHT,
pokemon = currentCategory.pokemon[leftOfCurrentSelection()],
clickAction = { _, _ -> this.left() },
rotationVector = this.modelWidget.rotVec
rotationVector = this.modelWidget.rotationVector
)
starterRoundaboutRight = StarterRoundabout(
@@ -177,7 +177,7 @@ class StarterSelectionScreen(private val categories: List<RenderableStarterCateg
pWidth = StarterRoundabout.MODEL_WIDTH, pHeight = StarterRoundabout.MODEL_HEIGHT,
pokemon = currentCategory.pokemon[rightOfCurrentSelection()],
clickAction = { _, _ -> this.right() },
rotationVector = this.modelWidget.rotVec
rotationVector = this.modelWidget.rotationVector
)
addRenderableWidget(starterRoundaboutLeft)
@@ -235,7 +235,8 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
pokemon = selectedPokemon.asRenderablePokemon(),
baseScale = 2F,
rotationY = 325F,
offsetY = -10.0
offsetY = -10.0,
shouldFollowCursor = true,
)
addRenderableOnly(this.modelWidget)
}
@@ -274,9 +275,11 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
listenToMoveSet()
displayMainScreen(mainScreenIndex)
children().find { it is EvolutionSelectScreen }?.let(this::removeWidget)
if (this::modelWidget.isInitialized) {
this.modelWidget.pokemon = selectedPokemon.asRenderablePokemon()
if (::modelWidget.isInitialized) {
modelWidget.pokemon = selectedPokemon.asRenderablePokemon()
}
if (this::nicknameEntryWidget.isInitialized) {
this.nicknameEntryWidget.setSelectedPokemon(selectedPokemon)
}
@@ -592,12 +595,6 @@ class Summary private constructor(party: Collection<Pokemon?>, private val edita
return children().any { it.mouseScrolled(mouseX, mouseY, amount, verticalAmount) }
}
/*
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
return children().any { it.mouseClicked(mouseX, mouseY, button) }
}
*/
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (sideScreenIndex == MOVE_SWAP || sideScreenIndex == EVOLVE) sideScreen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
if (mainScreenIndex == MOVES) mainScreen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
@@ -8,7 +8,9 @@
package com.cobblemon.mod.common.client.gui.summary.widgets
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.calculateHeadYawAndPitch
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
@@ -20,29 +22,39 @@ import org.joml.Vector3f
class ModelWidget(
pX: Int, pY: Int,
pWidth: Int, pHeight: Int,
var pokemon: RenderablePokemon,
pokemon: RenderablePokemon,
val baseScale: Float = 2.7F,
var rotationY: Float = 35F,
var offsetY: Double = 0.0,
val playCryOnClick: Boolean = false,
val shouldFollowCursor: Boolean = false,
): SoundlessWidget(pX, pY, pWidth, pHeight, Component.literal("Summary - ModelWidget")) {
companion object {
var render = true
const val MAX_SECONDS_LOOKING = 6L
}
var pokemon: RenderablePokemon = pokemon
set (value) {
field = value
currentYawAndPitch = Pair(0f, 0f)
}
var state = FloatingState()
val rotVec = Vector3f(13F, rotationY, 0F)
var lookStartTime: Long? = null
var currentYawAndPitch: Pair<Float, Float> = Pair(0f, 0f)
val rotationVector = Vector3f(13F, rotationY, 0F)
override fun renderWidget(context: GuiGraphics, pMouseX: Int, pMouseY: Int, partialTicks: Float) {
if (!render) {
return
}
isHovered = pMouseX >= x && pMouseY >= y && pMouseX < x + width && pMouseY < y + height
renderPKM(context, partialTicks)
renderPKM(context, partialTicks, pMouseX, pMouseY)
}
private fun renderPKM(context: GuiGraphics, partialTicks: Float) {
private fun renderPKM(context: GuiGraphics, partialTicks: Float, mouseX: Int, mouseY: Int) {
val matrices = context.pose()
matrices.pushPose()
@@ -50,19 +62,48 @@ class ModelWidget(
x,
y,
x + width,
y + height
y + height
)
matrices.translate(x + width * 0.5, y.toDouble() + offsetY, 0.0)
matrices.scale(baseScale, baseScale, baseScale)
matrices.pushPose()
if (isHovered) {
lookStartTime = System.currentTimeMillis()
}
val timeLooking = lookStartTime?.let { (System.currentTimeMillis() - it) / 1000 } ?: MAX_SECONDS_LOOKING
val lookedTooMuch = timeLooking >= MAX_SECONDS_LOOKING
val resetToCenter = (!isHovered && lookedTooMuch) || !shouldFollowCursor || !Cobblemon.config.summaryPokemonFollowCursor
if (resetToCenter) {
lookStartTime = null
}
val rotation = Quaternionf().fromEulerXYZDegrees(rotationVector)
if (shouldFollowCursor) {
currentYawAndPitch = calculateHeadYawAndPitch(
x + width / 2f,
y + height / 2f,
rotation,
mouseX,
mouseY,
currentYawAndPitch.first,
currentYawAndPitch.second,
resetToCenter
)
}
drawProfilePokemon(
renderablePokemon = pokemon,
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(rotVec),
rotation = rotation,
state = state,
partialTicks = partialTicks
partialTicks = partialTicks,
headYaw = currentYawAndPitch.first,
headPitch = currentYawAndPitch.second,
)
matrices.popPose()
@@ -72,7 +113,10 @@ class ModelWidget(
}
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (this.isHovered) playCry()
if (this.isHovered) {
playCry()
}
return super.mouseClicked(pMouseX, pMouseY, pButton)
}
@@ -9,14 +9,11 @@
package com.cobblemon.mod.common.client.net.pokemon.update
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.render.layer.PokemonOnShoulderRenderer
import com.cobblemon.mod.common.net.messages.client.PokemonUpdatePacket
import net.minecraft.client.Minecraft
class PokemonUpdatePacketHandler<T : PokemonUpdatePacket<T>> : ClientNetworkPacketHandler<T> {
override fun handle(packet: T, client: Minecraft) {
packet.applyToPokemon()
val player = client.player ?: return
PokemonOnShoulderRenderer.clearCache(player)
}
}
@@ -0,0 +1,196 @@
/*
* 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.block
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.block.entity.StadiumBlockEntity
import com.cobblemon.mod.common.client.render.models.blockbench.drone.DroneModel
import com.cobblemon.mod.common.client.render.models.blockbench.drone.DroneState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.math.Axis
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.core.Direction
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.level.block.HorizontalDirectionalBlock
import net.minecraft.world.level.block.state.BlockState
class StadiumBlockRenderer(ctx: BlockEntityRendererProvider.Context) : BlockEntityRenderer<StadiumBlockEntity> {
val context = RenderContext().also {
it.put(RenderContext.DO_QUIRKS, false)
it.put(RenderContext.RENDER_STATE, RenderContext.RenderState.BLOCK)
}
private var pathIndex = 0
private var animationTime = 0.0
private val droneSpeed = 0.10
override fun render(
blockEntity: StadiumBlockEntity,
tickDelta: Float,
poseStack: PoseStack,
multiBufferSource: MultiBufferSource,
light: Int,
overlay: Int
) {
val blockState = blockEntity.blockState
val yRot = blockState.getValue(HorizontalDirectionalBlock.FACING).toYRot()
val isClientSide = blockEntity.level?.isClientSide == true
if (!isClientSide) return
val stadiumBlockState = blockEntity.level?.getBlockState(blockEntity.blockPos) ?: return
if (stadiumBlockState.block != CobblemonBlocks.STADIUM) {
return
}
val droneState = blockEntity.droneState
poseStack.pushPose()
if (blockEntity.isInUse) {
if (blockEntity.pathPoints.isEmpty()) {
poseStack.popPose()
return
}
spawnDebugParticles(blockEntity) // debug particles
// Center the drone at the block's center
poseStack.translate(0.5, -3.0, 0.5)
//poseStack.mulPose(Axis.XP.rotationDegrees(180F))
//poseStack.mulPose(Axis.YP.rotationDegrees(180F))
val points = blockEntity.pathPoints
val nextIndex = (pathIndex + 1) % points.size
val currentPoint = points[pathIndex]
val nextPoint = points[nextIndex]
animationTime += tickDelta * droneSpeed
if (animationTime >= 1.0) {
animationTime = 0.0
pathIndex = nextIndex
}
val interpolatedPos = currentPoint.lerp(nextPoint, animationTime.toDouble())
println("DEBUG: Drone moving between points:")
println(" - Current Point: ${currentPoint}")
println(" - Next Point: ${nextPoint}")
println(" - Interpolated Position: $interpolatedPos")
poseStack.translate(interpolatedPos.x - blockEntity.blockPos.x,
interpolatedPos.y - blockEntity.blockPos.y,
interpolatedPos.z - blockEntity.blockPos.z)
println("DEBUG: Drone position interpolated to: $interpolatedPos")
poseStack.mulPose(Axis.YP.rotationDegrees(-yRot))
poseStack.mulPose(Axis.ZP.rotationDegrees(180F)) // flip the drone right side up
renderDrone(droneState, "sleep", tickDelta, poseStack, 1.0F, multiBufferSource, light)
} else {
val yRot = blockState.getValue(HorizontalDirectionalBlock.FACING).toYRot()
val poseEntry = poseStack.last()
val matrix = poseEntry.pose()
poseStack.translate(0.5, 0.0, 0.5)
poseStack.mulPose(Axis.YP.rotationDegrees(yRot))
val posX = matrix.m03()
val posY = matrix.m13()
val posZ = matrix.m23()
println("DEBUG: Drone is NOT in use. Rendering default position at: x=$posX, y=$posY, z=$posZ")
poseStack.mulPose(Axis.ZP.rotationDegrees(180F))
renderDrone(droneState, "sleep", tickDelta, poseStack, 1.0F, multiBufferSource, light)
}
poseStack.popPose()
}
fun renderDrone(
state: DroneState,
pose: String,
tickDelta: Float,
poseStack: PoseStack,
scale: Float = 1F,
multiBufferSource: MultiBufferSource,
light: Int,
) {
state.updatePartialTicks(tickDelta)
val droneResource: ResourceLocation = cobblemonResource("rotom_drone")
val model = VaryingModelRepository.getPoser(droneResource, state) as DroneModel
val texture = VaryingModelRepository.getTexture(droneResource, state)
val buffer = multiBufferSource.getBuffer(RenderType.entityCutout(texture))
model.bufferProvider = multiBufferSource
state.currentModel = model
state.setPose(pose)
model.context = context
context.put(RenderContext.TEXTURE, texture)
context.put(RenderContext.RENDER_STATE, RenderContext.RenderState.BLOCK)
context.put(RenderContext.POSABLE_STATE, state)
model.applyAnimations(
entity = null,
state = state,
headYaw = 0F,
headPitch = 0F,
limbSwing = 0F,
limbSwingAmount = 0F,
ageInTicks = state.animationSeconds * 20
)
poseStack.scale(scale, scale, scale)
// TODO: Make drone lighting dynamic (simply using the light arg makes the drone always appear dark)
model.withLayerContext(multiBufferSource, state, VaryingModelRepository.getLayers(droneResource, state)) {
model.render(context, poseStack, buffer, 200, OverlayTexture.NO_OVERLAY, -0x1)
}
model.setDefault()
}
private fun spawnDebugParticles(blockEntity: StadiumBlockEntity) {
val world = blockEntity.level ?: return
if (!world.isClientSide) return // Only run on the client-side
for (point in blockEntity.pathPoints) {
world.addParticle(
net.minecraft.core.particles.ParticleTypes.FLAME, // Test particle (changeable)
point.x, point.y, point.z, // Position of the particle
0.0, 0.02, 0.0 // Slight upward motion
)
}
// Spawn a special particle at the next interpolation point
if (blockEntity.pathPoints.isNotEmpty()) {
val nextPoint = blockEntity.pathPoints[(pathIndex + 1) % blockEntity.pathPoints.size]
world.addParticle(
net.minecraft.core.particles.ParticleTypes.END_ROD, // Different effect for next point
nextPoint.x, nextPoint.y, nextPoint.z,
0.0, 0.05, 0.0
)
}
}
}
@@ -87,24 +87,18 @@ class PokemonOnShoulderRenderer<T : Player>(renderLayerParent: RenderLayerParent
) {
val compoundTag = if (pLeftShoulder) livingEntity.shoulderEntityLeft else livingEntity.shoulderEntityRight
if (compoundTag.isPokemonEntity()) {
matrixStack.pushPose()
livingEntity.level().profiler.push("shoulder_render_".plus(if(pLeftShoulder) "left" else "right"))
val uuid = this.extractUuid(compoundTag)
val cache = playerCache.getOrPut(livingEntity.uuid) { ShoulderCache() }
var shoulderData: ShoulderData? = null
if (pLeftShoulder && cache.lastKnownLeft?.uuid != uuid) {
shoulderData = this.extractData(compoundTag, uuid)
cache.lastKnownLeft = shoulderData
}
else if (!pLeftShoulder && cache.lastKnownRight?.uuid != uuid) {
shoulderData = this.extractData(compoundTag, uuid)
cache.lastKnownRight = shoulderData
}
val shoulderData = this.extractData(compoundTag, uuid)
if (pLeftShoulder) cache.lastKnownLeft = shoulderData else cache.lastKnownRight = shoulderData
if (shoulderData == null){
// Could be null
shoulderData = (if (pLeftShoulder) cache.lastKnownLeft else cache.lastKnownRight) ?: return
// Nothing to do
return
}
matrixStack.pushPose()
var state = FloatingState()
state.currentAspects = shoulderData.aspects
val model = VaryingModelRepository.getPoser(shoulderData.species.resourceIdentifier, state)
@@ -129,6 +123,7 @@ class PokemonOnShoulderRenderer<T : Player>(renderLayerParent: RenderLayerParent
)
matrixStack.scale(scale, scale, scale)
matrixStack.translate(0f, 1.5f, 0f)
state = if (pLeftShoulder && shoulderData != lastRenderedLeft) {
leftState = configureState(state, model, true)
@@ -162,6 +157,11 @@ class PokemonOnShoulderRenderer<T : Player>(renderLayerParent: RenderLayerParent
}
model.setDefault()
matrixStack.popPose()
livingEntity.level().profiler.pop()
}
else {
val entry = playerCache.getOrPut(livingEntity.uuid) { ShoulderCache() }
if (pLeftShoulder) entry.lastKnownLeft = null else entry.lastKnownRight = null
}
}
@@ -217,16 +217,5 @@ class PokemonOnShoulderRenderer<T : Player>(renderLayerParent: RenderLayerParent
return Pair(cache.lastKnownLeft, cache.lastKnownRight)
}
/**
* Clears the cache for the respective player.
* This is required to be done whenever a Pokémon changes in any way (form, evolution, etc.)
*
* @param player The player whose cache to clear
*/
@JvmStatic
fun clearCache(player: Player) {
playerCache.remove(player.uuid)
}
}
}
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2025 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.drone
import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PoseAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Bone
import com.cobblemon.mod.common.client.render.models.blockbench.quirk.ModelQuirk
import com.cobblemon.mod.common.entity.PoseType
import com.google.gson.annotations.SerializedName
import net.minecraft.client.model.geom.ModelPart
/**
* A model for rendering the Rotom drone used by the stadium block.
*
* @author Flarkinater
* @since February 14th, 2025
*/
class DroneModel(root: Bone) : PosableModel(root) {
@Transient
@SerializedName("So this name can be literally anything, huh?")
override val rootPart = (root as ModelPart).registerChildWithAllChildren(root.children.entries.first().key)
var droneAnimations: Array<PoseAnimation> = emptyArray()
var droneQuirks: Array<ModelQuirk<*>> = emptyArray()
override fun registerPoses() {
registerPose(
poseType = PoseType.SLEEP,
animations = droneAnimations,
quirks = droneQuirks
)
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2025 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.drone
import com.cobblemon.mod.common.api.scheduling.SchedulingTracker
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
/**
* State for a drone Rotom attached to a stadium block.
*
* @author Flarkinater
* @since February 12th, 2025
*/
class DroneState() : PosableState() {
init {
setPose("sleep")
age = 0
currentPartialTicks = 0F
}
override fun getEntity() = null
override fun updatePartialTicks(partialTicks: Float) {
currentPartialTicks += partialTicks / 2
}
override val schedulingTracker = SchedulingTracker()
}
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.client.render.models.blockbench.*
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimationRepository
import com.cobblemon.mod.common.client.render.models.blockbench.blockentity.BlockEntityModel
import com.cobblemon.mod.common.client.render.models.blockbench.fossil.FossilModel
import com.cobblemon.mod.common.client.render.models.blockbench.drone.DroneModel
import com.cobblemon.mod.common.client.render.models.blockbench.pokeball.AncientPokeBallModel
import com.cobblemon.mod.common.client.render.models.blockbench.pokeball.BeastBallModel
import com.cobblemon.mod.common.client.render.models.blockbench.pokeball.PokeBallModel
@@ -79,12 +80,14 @@ object VaryingModelRepository {
"poke_balls",
"generic",
"block_entities",
"drone"
)
val poserDirectories: List<Pair<String, Class<out PosableModel>>> = listOf(
"bedrock/posers" to PosableModel::class.java,
"bedrock/pokemon/posers" to PokemonPosableModel::class.java,
"bedrock/fossils/posers" to FossilModel::class.java,
"bedrock/drone/posers" to DroneModel::class.java,
"bedrock/block_entities/posers" to BlockEntityModel::class.java,
"bedrock/npcs/posers" to PosableModel::class.java,
"bedrock/poke_balls/posers" to PosableModel::class.java,
@@ -116,6 +119,7 @@ object VaryingModelRepository {
.registerTypeAdapter(PosableModel::class.java, JsonModelAdapter(::PosableModel))
.registerTypeAdapter(PokemonPosableModel::class.java, JsonModelAdapter(::PokemonPosableModel))
.registerTypeAdapter(FossilModel::class.java, JsonModelAdapter(::FossilModel))
.registerTypeAdapter(DroneModel::class.java, JsonModelAdapter(::DroneModel))
.registerTypeAdapter(BlockEntityModel::class.java, JsonModelAdapter(::BlockEntityModel))
.registerTypeAdapter(Pose::class.java, PoseAdapter { JsonModelAdapter.model!! })
.registerTypeAdapter(ModelPartTransformation::class.java, ModelPartTransformationAdapter)
@@ -21,8 +21,9 @@ import kotlin.random.Random
abstract class PokeBallPosableState : PosableState(), Schedulable {
abstract val stateEmitter: SettableObservable<EmptyPokeBallEntity.CaptureState>
abstract val shakeEmitter: Observable<Unit>
private val group: String
protected open val group: String
get() = if (this.currentModel is AncientPokeBallModel) "ancient_poke_ball" else "poke_ball"
protected open var lastKnownState: EmptyPokeBallEntity.CaptureState? = null
open fun initSubscriptions() {
stateEmitter.subscribe { state ->
@@ -44,9 +45,17 @@ abstract class PokeBallPosableState : PosableState(), Schedulable {
}
EmptyPokeBallEntity.CaptureState.FALL -> {}
EmptyPokeBallEntity.CaptureState.SHAKE -> {
EmptyPokeBallEntity.CaptureState.CRITICAL -> {
doLater {
setActiveAnimations(currentModel!!.bedrockStateful(group, "bounce"))
setActiveAnimations(currentModel!!.bedrockStateful(group, "critical"))
}
}
EmptyPokeBallEntity.CaptureState.SHAKE -> {
if (this.lastKnownState != EmptyPokeBallEntity.CaptureState.CRITICAL) {
// Skip the bounce on a critical capture, the animation replaces this step
doLater {
setActiveAnimations(currentModel!!.bedrockStateful(group, "bounce"))
}
}
shakeEmitter
.pipe(Observable.emitWhile { stateEmitter.get() == EmptyPokeBallEntity.CaptureState.SHAKE })
@@ -60,11 +69,6 @@ abstract class PokeBallPosableState : PosableState(), Schedulable {
setActiveAnimations(currentModel!!.bedrockStateful(group, "capture"))
}
}
EmptyPokeBallEntity.CaptureState.CAPTURED_CRITICAL -> {
doLater {
setActiveAnimations(currentModel!!.bedrockStateful(group, "critical"))
}
}
EmptyPokeBallEntity.CaptureState.BROKEN_FREE -> {
doLater {
setActiveAnimations(currentModel!!.bedrockStateful(group, "break"))
@@ -72,6 +76,7 @@ abstract class PokeBallPosableState : PosableState(), Schedulable {
}
else -> {}
}
this.lastKnownState = state
}
}
}
@@ -18,5 +18,6 @@ enum class Category(val lang: String) {
Pokedex("pokedex"),
Storage("storage"),
Starter("starter"),
Interface("interface"),
Debug("debug"),
}
@@ -182,7 +182,7 @@ class CobblemonConfig {
var displayNameForUnknownPokemon = false
@CobblemonConfigField(Category.Pokemon, lang = "display_entity_labels_when_crouching_only")
var displayEntityLabelsWhenCrouchingOnly = true
var displayEntityLabelsWhenCrouchingOnly = false
@CobblemonConfigField(Category.Spawning, lang = "shiny_rate")
var shinyRate = 8192F
@@ -252,6 +252,9 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Pokedex, lang = "hide_unimplemented_pokemon_in_the_pokedex")
var hideUnimplementedPokemonInThePokedex = false
@CobblemonConfigField(Category.Interface, lang = "summary_pokemon_follow_cursor")
var summaryPokemonFollowCursor = true
@CobblemonConfigField(Category.Debug, lang = "enable_debug_keys")
var enableDebugKeys = false
@@ -72,16 +72,15 @@ import net.minecraft.world.phys.EntityHitResult
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import java.util.concurrent.CompletableFuture
import kotlin.compareTo
class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragModifier, Schedulable {
enum class CaptureState {
NOT,
HIT,
FALL,
CRITICAL,
SHAKE,
CAPTURED,
CAPTURED_CRITICAL,
BROKEN_FREE
}
@@ -138,14 +137,13 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
delegate.onSyncedDataUpdated(data)
}
if (data == CAPTURE_STATE) {
val newState = entityData.get(CAPTURE_STATE)
val newState = entityData[CAPTURE_STATE]
when (CaptureState.entries[newState.toInt()]) {
CaptureState.NOT -> setNoGravity(false)
CaptureState.HIT -> {}
CaptureState.FALL -> setNoGravity(false)
CaptureState.SHAKE -> setNoGravity(true)
CaptureState.CAPTURED, CaptureState.CAPTURED_CRITICAL -> {}
CaptureState.BROKEN_FREE -> {}
CaptureState.NOT, CaptureState.FALL -> isNoGravity = false
CaptureState.CRITICAL, CaptureState.SHAKE -> isNoGravity = true
CaptureState.HIT, CaptureState.CAPTURED, CaptureState.BROKEN_FREE -> {
// no op
}
}
}
dataTrackerEmitter.emit(data)
@@ -339,14 +337,14 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
if (rollsRemaining <= 0) {
if (captureResult.isSuccessfulCapture) {
captureState = if (captureResult.isCriticalCapture) CaptureState.CAPTURED_CRITICAL else CaptureState.CAPTURED
captureState = CaptureState.CAPTURED
// Do a capture
val pokemon = capturingPokemon ?: return
val player = this.owner as? ServerPlayer ?: return
val captureTime = if (pokeBall.ancient == true) 1.8F else 1F
after(seconds = captureTime) {
// Dupes occurred by double-adding Pokémon, this hopefully prevents it triple-condom style
// wtf did I just read above me
if (pokemon.pokemon.isWild() && pokemon.isAlive && !captureFuture.isDone) {
pokemon.discard()
discard()
@@ -406,7 +404,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
pokemon.busyLocks.remove(this)
captureFuture.complete(false)
val ballType =
this.pokeBall.name.path.toString().toLowerCase().replace("_", "")
this.pokeBall.name.path.lowercase().replace("_", "")
val mode = "casual"
val sendflash =
BedrockParticleOptionsRepository.getEffect(cobblemonResource("${ballType}/${mode}/sendflash"))
@@ -473,7 +471,6 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
override fun onHit(hitResult: HitResult) {
super.onHit(hitResult)
if (captureState == CaptureState.FALL && hitResult.type == HitResult.Type.BLOCK) {
captureState = CaptureState.SHAKE
if (level().isServerSide()) {
beginCapture()
}
@@ -494,12 +491,26 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
if (rollsRemaining == 4) {
rollsRemaining--
}
var animatedCritical = false
// Critical capture despite their name actually animate first
// They can still fail and don't actually signify a success
val iterations: Int = if (captureResult.isCriticalCapture) {
captureState = CaptureState.CRITICAL
captureResult.numberOfShakes + 2
} else {
captureState = CaptureState.SHAKE
captureResult.numberOfShakes + 1
}
taskBuilder()
.iterations(captureResult.numberOfShakes + 1)
.iterations(iterations)
.delay(SECONDS_BEFORE_SHAKE)
.interval(SECONDS_BETWEEN_SHAKES)
.execute {
if (captureResult.isCriticalCapture && !animatedCritical) {
captureState = CaptureState.SHAKE
animatedCritical = true
return@execute
}
if (pokeBall.ancient && rollsRemaining > 1) {
rollsRemaining = 1
}
@@ -1178,12 +1178,17 @@ open class PokemonEntity(
if (!super.setEntityOnShoulder(player)) {
return false
}
var isLeft = false
// Use copies because player doesn't expose a forceful update of shoulder data
val nbt = when {
player.shoulderEntityRight.isPokemonEntity() && player.shoulderEntityRight.getCompound(DataKeys.POKEMON)
.getUUID(DataKeys.POKEMON_UUID) == this.pokemon.uuid -> player.shoulderEntityRight
.getUUID(DataKeys.POKEMON_UUID) == this.pokemon.uuid -> player.shoulderEntityRight.copy()
player.shoulderEntityLeft.isPokemonEntity() && player.shoulderEntityLeft.getCompound(DataKeys.POKEMON)
.getUUID(DataKeys.POKEMON_UUID) == this.pokemon.uuid -> player.shoulderEntityLeft
.getUUID(DataKeys.POKEMON_UUID) == this.pokemon.uuid -> {
isLeft = true
player.shoulderEntityLeft.copy()
}
else -> return true
}
@@ -1192,6 +1197,7 @@ open class PokemonEntity(
nbt.putString(DataKeys.SHOULDER_FORM, this.pokemon.form.name)
nbt.put(DataKeys.SHOULDER_ASPECTS, this.pokemon.aspects.map(StringTag::valueOf).toNbtList())
nbt.putFloat(DataKeys.SHOULDER_SCALE_MODIFIER, this.pokemon.scaleModifier)
if (isLeft) player.shoulderEntityLeft = nbt else player.shoulderEntityRight = nbt
return true
}
@@ -238,6 +238,7 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.PC)
entries.accept(CobblemonItems.HEALING_MACHINE)
entries.accept(CobblemonItems.PASTURE)
entries.accept(CobblemonItems.STADIUM)
entries.accept(CobblemonItems.GILDED_CHEST)
entries.accept(CobblemonItems.YELLOW_GILDED_CHEST)
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.pokeball.catching.CaptureContext
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CriticalCaptureProvider
import com.cobblemon.mod.common.api.pokeball.catching.calculators.PokedexProgressCaptureMultiplierProvider
import com.cobblemon.mod.common.api.pokeball.catching.calculators.PokedexStatusCaptureInfluencer
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
@@ -32,7 +33,7 @@ import kotlin.random.Random
import net.minecraft.world.entity.LivingEntity
import net.minecraft.server.level.ServerPlayer
object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, PokedexProgressCaptureMultiplierProvider {
object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, PokedexProgressCaptureMultiplierProvider, PokedexStatusCaptureInfluencer {
override fun id(): String = "cobblemon"
@@ -54,9 +55,9 @@ object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, P
val pokeBall = pokeBallEntity.pokeBall
val pokemon = target.pokemon
if (pokeBall.catchRateModifier.isGuaranteed()) {
return CaptureContext.successful()
return this.influence(thrower, target, CaptureContext.successful())
}
// We don't have dark grass so we're just gonna pretend everything is that. Scratch that, without the pokedex it has issues.
// We don't have dark grass so we're just going to pretend everything is that. Scratch that, without the Pokédex it has issues.
val darkGrass = 1F //if (thrower is ServerPlayer) this.caughtMultiplierFor(thrower).roundToInt() else 1
val inBattleModifier = if (target.battleId != null) 1F else 0.5F
val catchRate = getCatchRate(thrower, pokeBallEntity, target, pokemon.form.catchRate.toFloat())
@@ -94,8 +95,8 @@ object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, P
return CaptureContext(numberOfShakes = 1, isSuccessfulCapture = shakes == 1, isCriticalCapture = true)
}
}
// ToDo once pokedex is implemented if the target is registered and it's a success shorten to 1 shake and become critical capture
return CaptureContext(numberOfShakes = shakes, isSuccessfulCapture = shakes == 4, isCriticalCapture = false)
return this.influence(thrower, target, CaptureContext(numberOfShakes = shakes, isSuccessfulCapture = shakes == 4, isCriticalCapture = false))
}
private fun findHighestThrowerLevel(player: ServerPlayer, pokemon: Pokemon): Int? {
@@ -109,4 +110,5 @@ object CobblemonCaptureCalculator: CaptureCalculator, CriticalCaptureProvider, P
} ?: return null
return actor.getSide().getOppositeSide().activePokemon.maxOfOrNull { it.battlePokemon?.effectedPokemon?.level ?: 1 }
}
}
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.pokeball.catching.CaptureContext
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CriticalCaptureProvider
import com.cobblemon.mod.common.api.pokeball.catching.calculators.PokedexProgressCaptureMultiplierProvider
import com.cobblemon.mod.common.api.pokeball.catching.calculators.PokedexStatusCaptureInfluencer
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.status.statuses.persistent.BurnStatus
@@ -34,7 +35,7 @@ import net.minecraft.server.level.ServerPlayer
* @author Licious
* @since January 29th, 2022
*/
object Gen9CaptureCalculator : CaptureCalculator, CriticalCaptureProvider, PokedexProgressCaptureMultiplierProvider {
object Gen9CaptureCalculator : CaptureCalculator, CriticalCaptureProvider, PokedexProgressCaptureMultiplierProvider, PokedexStatusCaptureInfluencer {
override fun id(): String = "generation_9"
@@ -42,7 +43,7 @@ object Gen9CaptureCalculator : CaptureCalculator, CriticalCaptureProvider, Poked
val pokeBall = pokeBallEntity.pokeBall
val pokemon = target.pokemon
if (pokeBall.catchRateModifier.isGuaranteed()) {
return CaptureContext.successful()
return this.influence(thrower, target, CaptureContext.successful())
}
// We don't have dark grass so we're just gonna pretend everything is that.
val darkGrass = if (thrower is ServerPlayer) this.caughtMultiplierFor(thrower).roundToInt() else 1
@@ -70,8 +71,7 @@ object Gen9CaptureCalculator : CaptureCalculator, CriticalCaptureProvider, Poked
return CaptureContext(numberOfShakes = 1, isSuccessfulCapture = shakes == 1, isCriticalCapture = true)
}
}
// ToDo once pokedex is implemented if the target is registered and it's a success shorten to 1 shake and become critical capture
return CaptureContext(numberOfShakes = shakes, isSuccessfulCapture = shakes == 4, isCriticalCapture = false)
return this.influence(thrower, target, CaptureContext(numberOfShakes = shakes, isSuccessfulCapture = shakes == 4, isCriticalCapture = false))
}
}
@@ -97,7 +97,6 @@ import com.cobblemon.mod.common.pokemon.properties.BattleCloneProperty
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.status.PersistentStatus
import com.cobblemon.mod.common.pokemon.status.PersistentStatusContainer
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.codec.internal.ClientPokemonP1
import com.cobblemon.mod.common.util.codec.internal.ClientPokemonP2
@@ -128,7 +127,6 @@ import net.minecraft.core.BlockPos
import net.minecraft.core.RegistryAccess
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.StringTag
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.contents.PlainTextContents
@@ -25,10 +25,12 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.PokedexItem
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.activestate.ShoulderedState
import com.cobblemon.mod.common.trade.TradeManager
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.nbt.StringTag
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
@@ -457,4 +459,15 @@ fun Player.isPartyBusy() =
fun Player.isUsingPokedex() = isUsingItem &&
((mainHandItem.item is PokedexItem && usedItemHand == InteractionHand.MAIN_HAND) ||
(offhandItem.item is PokedexItem && usedItemHand == InteractionHand.OFF_HAND))
(offhandItem.item is PokedexItem && usedItemHand == InteractionHand.OFF_HAND))
fun ServerPlayer.updateShoulderNbt(pokemon: Pokemon) {
// Use copies because player doesn't expose a forceful update of shoulder data
val nbt = if ((pokemon.state as ShoulderedState).isLeftShoulder) shoulderEntityLeft.copy() else shoulderEntityRight.copy()
nbt.putUUID(DataKeys.SHOULDER_UUID, uuid)
nbt.putString(DataKeys.SHOULDER_SPECIES, pokemon.species.resourceIdentifier.toString())
nbt.putString(DataKeys.SHOULDER_FORM, pokemon.form.name)
nbt.put(DataKeys.SHOULDER_ASPECTS, pokemon.aspects.map(StringTag::valueOf).toNbtList())
nbt.putFloat(DataKeys.SHOULDER_SCALE_MODIFIER, pokemon.scaleModifier)
if ((pokemon.state as ShoulderedState).isLeftShoulder) shoulderEntityLeft = nbt else shoulderEntityRight = nbt
}
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.util.math
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import kotlin.math.cos
import kotlin.math.sin
import org.joml.Quaternionf
@@ -38,4 +39,18 @@ fun Quaternionf.hamiltonProduct(other: Quaternionf): Quaternionf {
this.z = i * l + f * k - g * j + h * m
this.w = i * m - f * j - g * k - h * l
return this
}
fun Quaternionf.toEulerXYZ(): Vector3f {
return getEulerAnglesXYZ(Vector3f(0F, 0F, 0F))
}
fun Quaternionf.toEulerXYZDegrees(): Vector3f {
val vector = toEulerXYZ()
vector.x = vector.x.toDegrees()
vector.y = vector.y.toDegrees()
vector.z = vector.z.toDegrees()
return vector
}
@@ -0,0 +1,41 @@
{
"format_version": "1.8.0",
"animations": {
"animation.rotom_drone.sleep": {
"loop": true,
"bones": {
"body": {
"rotation": [
"math.sin(query.anim_time * 120) * 6",
0,
"math.cos(query.anim_time * 100) * 4"
],
"position": [
0,
"18 + math.sin(query.anim_time * 90) * 1",
0
]
},
"head": {
"relative_to": {
"rotation": "entity"
},
"rotation": [
0,
0,
0.01
]
}
}
},
"animation.rotom_drone.circle": {
"loop": true,
"bones": {
"rotom_drone": {
"rotation": [30, "90 - q.anim_time*90", 0],
"position": ["48*math.cos(q.anim_time*90)", 64, "48*math.sin(q.anim_time*90)"]
}
}
}
}
}
@@ -0,0 +1,208 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.drone_rotom",
"texture_width": 64,
"texture_height": 32,
"visible_bounds_width": 8,
"visible_bounds_height": 6.5,
"visible_bounds_offset": [0, 2.75, 0]
},
"bones": [
{
"name": "rotom_drone",
"pivot": [0, 0, 0]
},
{
"name": "body",
"parent": "rotom_drone",
"pivot": [0, 8, 0]
},
{
"name": "head",
"parent": "body",
"pivot": [0, 6.5, 0],
"cubes": [
{"origin": [-5.5, 6.5, -3.5], "size": [11, 6, 7], "uv": [0, 0]}
]
},
{
"name": "eyes",
"parent": "head",
"pivot": [0.5, 6.5, -2.5]
},
{
"name": "eye_right",
"parent": "eyes",
"pivot": [-2.9, 9.5, -3.5],
"rotation": [0, 0, -7.5],
"cubes": [
{"origin": [-3.9, 7.5, -3.5], "size": [2, 4, 0], "inflate": 0.01, "uv": [0, 0], "mirror": true},
{"origin": [-3.9, 9, -3.5], "size": [2, 1, 0], "inflate": 0.02, "uv": [0, 6], "mirror": true}
]
},
{
"name": "pupil_right",
"parent": "eye_right",
"pivot": [-2.9, 9.5, -3.5],
"cubes": [
{"origin": [-3.4, 8, -3.5], "size": [1, 3, 0], "inflate": 0.03, "uv": [5, 0], "mirror": true}
]
},
{
"name": "eye_left",
"parent": "eyes",
"pivot": [2.9, 9.5, -3.5],
"rotation": [0, 0, 7.5],
"cubes": [
{"origin": [1.9, 7.5, -3.5], "size": [2, 4, 0], "inflate": 0.01, "uv": [0, 13]},
{"origin": [1.9, 9, -3.5], "size": [2, 1, 0], "inflate": 0.02, "uv": [0, 5]}
]
},
{
"name": "pupil_left",
"parent": "eye_left",
"pivot": [2.9, 9.5, -3.5],
"cubes": [
{"origin": [2.4, 8, -3.5], "size": [1, 3, 0], "inflate": 0.03, "uv": [6, 13]}
]
},
{
"name": "mouth_smile",
"parent": "head",
"pivot": [0, 7.55, -3.5],
"cubes": [
{"origin": [-1, 6.95, -3.5], "size": [2, 1, 0], "inflate": 0.02, "uv": [36, 7]},
{"origin": [-1.5, 7.25, -3.5], "size": [1, 1, 0], "inflate": 0.01, "uv": [40, 7]},
{"origin": [0.5, 7.25, -3.5], "size": [1, 1, 0], "inflate": 0.01, "uv": [42, 7]}
]
},
{
"name": "mouth_closed",
"parent": "head",
"pivot": [0, 7.45, -3.4],
"cubes": [
{"origin": [-0.5, 6.95, -3.4], "size": [1, 1, 0], "inflate": 0.02, "uv": [38, 8]}
]
},
{
"name": "mouth_open",
"parent": "head",
"pivot": [0, 7.45, -3.4],
"cubes": [
{"origin": [-0.5, 6.95, -3.4], "size": [1, 1, 0], "inflate": 0.02, "uv": [36, 8]}
]
},
{
"name": "mouth_sad",
"parent": "head",
"pivot": [0, 7.6, -3.3],
"cubes": [
{"origin": [-1, 7.3, -3.3], "size": [2, 1, 0], "inflate": 0.02, "uv": [36, 9]},
{"origin": [-1.5, 7, -3.3], "size": [1, 1, 0], "inflate": 0.01, "uv": [40, 9]},
{"origin": [0.5, 7, -3.3], "size": [1, 1, 0], "inflate": 0.01, "uv": [42, 9]}
]
},
{
"name": "head_spike1",
"parent": "head",
"pivot": [0, 12.5, 0],
"cubes": [
{"origin": [-1.5, 11.5, -1.5], "size": [3, 4, 3], "uv": [29, 0]}
]
},
{
"name": "head_spike2",
"parent": "head_spike1",
"pivot": [0, 15.5, 0],
"cubes": [
{"origin": [-1, 14.5, -1], "size": [2, 4, 2], "uv": [8, 13]}
]
},
{
"name": "head_spike3",
"parent": "head_spike2",
"pivot": [0, 18.5, 0],
"cubes": [
{"origin": [-0.5, 17.5, -0.5], "size": [1, 3, 1], "uv": [44, 6]}
]
},
{
"name": "torso",
"parent": "body",
"pivot": [0, 7, 0],
"cubes": [
{"origin": [-2, 3, -1.5], "size": [4, 4, 3], "uv": [34, 10]}
]
},
{
"name": "camera",
"parent": "torso",
"pivot": [0, 4.65, -1.25],
"cubes": [
{"origin": [-1, 3.65, -2.75], "size": [2, 2, 2], "uv": [24, 13]},
{"origin": [-0.5, 4.15, -2.75], "size": [1, 1, 0], "inflate": 0.01, "uv": [38, 0]}
]
},
{
"name": "propeller_front_right",
"parent": "torso",
"pivot": [-1.5, 5, -1],
"rotation": [0, 45, 0],
"cubes": [
{"origin": [-2, 5, -3.5], "size": [1, 0, 3], "uv": [37, 0]},
{"origin": [-5.5, 5, -11.5], "size": [8, 0, 8], "uv": [40, 0]}
]
},
{
"name": "propeller_front_left",
"parent": "torso",
"pivot": [1.5, 5, -1],
"rotation": [0, -45, 0],
"cubes": [
{"origin": [1, 5, -3.5], "size": [1, 0, 3], "uv": [27, 0]},
{"origin": [-2.5, 5, -11.5], "size": [8, 0, 8], "uv": [40, 0]}
]
},
{
"name": "propeller_back_right",
"parent": "torso",
"pivot": [-1.5, 5, 1],
"rotation": [0, -45, 0],
"cubes": [
{"origin": [-2, 5, 0.5], "size": [1, 0, 3], "uv": [39, 0]},
{"origin": [-5.5, 5, 3.5], "size": [8, 0, 8], "uv": [40, 0]}
]
},
{
"name": "propeller_back_left",
"parent": "torso",
"pivot": [1.5, 5, 1],
"rotation": [0, 45, 0],
"cubes": [
{"origin": [1, 5, 0.5], "size": [1, 0, 3], "uv": [42, 3]},
{"origin": [-2.5, 5, 3.5], "size": [8, 0, 8], "uv": [40, 0]}
]
},
{
"name": "tail1",
"parent": "torso",
"pivot": [0, 3, 0],
"cubes": [
{"origin": [-1, 1, -1], "size": [2, 3, 2], "uv": [16, 13]}
]
},
{
"name": "tail2",
"parent": "tail1",
"pivot": [0, 1, 0],
"cubes": [
{"origin": [-0.5, -1, -0.5], "size": [1, 3, 1], "uv": [41, 3]}
]
}
]
}
]
}
@@ -0,0 +1,8 @@
{
"poses": {
"sleep": {
"poseTypes": ["SLEEP"],
"animations": ["q.bedrock('rotom_drone', 'sleep')"]
}
}
}
@@ -0,0 +1,13 @@
{
"name": "cobblemon:rotom_drone",
"order": 0,
"variations": [
{
"aspects": [],
"poser": "cobblemon:rotom_drone",
"model": "cobblemon:rotom_drone.geo",
"texture": "cobblemon:textures/drone/rotom_drone.png",
"layers": []
}
]
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "cobblemon:block/stadium_terminal"
}
}
}
@@ -801,6 +801,7 @@
"block.cobblemon.healing_machine.tooltip2": "Charges over time.",
"block.cobblemon.pc": "PC",
"block.cobblemon.lectern": "Lectern",
"block.cobblemon.stadium_terminal": "Stadium Terminal",
"block.cobblemon.restoration_tank": "Restoration Tank",
"block.cobblemon.fossil_analyzer": "Fossil Analyzer",
"block.cobblemon.monitor": "Data Monitor",
@@ -1547,6 +1548,7 @@
"cobblemon.config.ui.category.healing": "Healing",
"cobblemon.config.ui.category.storage": "Storage",
"cobblemon.config.ui.category.world": "World",
"cobblemon.config.ui.category.interface": "Interface",
"cobblemon.config.ui.category.debug": "Debug",
"cobblemon.config.ui.max_pokemon_level": "Max Pokémon Level",
@@ -1693,6 +1695,8 @@
"cobblemon.config.ui.max_pokedex_scanning_detection_range.tooltip": "The maximum distance at which the Pokédex can detect and scan Pokémon.",
"cobblemon.config.ui.hide_unimplemented_pokemon_in_the_pokedex": "Hide Unimplemented Pokémon in the Pokédex",
"cobblemon.config.ui.hide_unimplemented_pokemon_in_the_pokedex.tooltip": "Whether to hide unimplemented Pokémon from the Pokédex.",
"cobblemon.config.ui.summary_pokemon_follow_cursor": "Make Pokémon Follow Cursor",
"cobblemon.config.ui.summary_pokemon_follow_cursor.tooltip": "Whether or not Pokémon should follow the mouse cursor with their heads in the Summary screen.",
"cobblemon.config.ui.enable_debug_keys": "Enable Debug Keys",
"cobblemon.config.ui.enable_debug_keys.tooltip": "Enables additional commands and chat logging to assist with implementing Pokémon (see the wiki for a list of commands).",
@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "cobblemon:block/stadium_terminal"
}
}
@@ -0,0 +1,3 @@
{
"parent": "cobblemon:block/stadium_terminal"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,13 @@
{
"type": "minecraft:block",
"pools": [{
"rolls": 1,
"entries": [{
"type": "minecraft:item",
"name": "cobblemon:stadium_terminal"
}],
"conditions": [{
"condition": "minecraft:survives_explosion"
}]
}]
}