Files
cobblemon/kt_only_tm.patch
T

11360 lines
1001 KiB
Diff

diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/Cobblemon.kt b/common/src/main/kotlin/com/cobblemon/mod/common/Cobblemon.kt
index 1178252d98..ce511a54e2 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/Cobblemon.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/Cobblemon.kt
@@ -62,6 +62,8 @@ import com.cobblemon.mod.common.api.storage.player.adapter.DexDataMongoBackend
import com.cobblemon.mod.common.api.storage.player.adapter.DexDataNbtBackend
import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataJsonBackend
import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataMongoBackend
+import com.cobblemon.mod.common.api.storage.player.adapter.TMMoveMongoBackend
+import com.cobblemon.mod.common.api.storage.player.adapter.TMMoveNbtBackend
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
import com.cobblemon.mod.common.api.tags.CobblemonEntityTypeTags
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
@@ -109,13 +111,7 @@ import com.cobblemon.mod.common.pokemon.properties.UnaspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
-import com.cobblemon.mod.common.util.DataKeys
-import com.cobblemon.mod.common.util.cobblemonResource
-import com.cobblemon.mod.common.util.ifDedicatedServer
-import com.cobblemon.mod.common.util.isLaterVersion
-import com.cobblemon.mod.common.util.party
-import com.cobblemon.mod.common.util.requestWallpapers
-import com.cobblemon.mod.common.util.server
+import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.world.feature.CobblemonPlacedFeatures
import com.cobblemon.mod.common.world.feature.ore.CobblemonOrePlacedFeatures
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
@@ -244,6 +240,8 @@ object Cobblemon {
DATA_SYNCHRONIZED.subscribe {
storage.onPlayerDataSync(it)
+ it.tmList().scheduleFullSyncFromStores(it.party(), it.pc())
+ it.pokedex().scheduleFullSyncFromStores(it.party(), it.pc())
playerDataManager.syncAllToPlayer(it)
starterHandler.handleJoin(it)
it.requestWallpapers()
@@ -306,6 +304,7 @@ object Cobblemon {
this.config.maxPokemonLevel,
this.config.maxPokemonFriendship,
this.config.maxDynamaxLevel,
+ this.config.unlockAllMoveDexMovesByDefault
).sendToPlayer(player)
}
@@ -375,8 +374,12 @@ object Cobblemon {
val pokedexNbtFactory = CachedPlayerDataStoreFactory(DexDataNbtBackend())
pokedexNbtFactory.setup(server)
+ val tmMoveFactory = CachedPlayerDataStoreFactory(TMMoveNbtBackend())
+ tmMoveFactory.setup(server)
+
playerDataManager.setFactory(generalJsonFactory, PlayerInstancedDataStoreTypes.GENERAL)
playerDataManager.setFactory(pokedexNbtFactory, PlayerInstancedDataStoreTypes.POKEDEX)
+ playerDataManager.setFactory(tmMoveFactory, PlayerInstancedDataStoreTypes.TM_MOVES)
if (config.storageFormat == "nbt") {
NBTStoreAdapter(pokemonStoreRoot.absolutePath, useNestedFolders = true, folderPerClass = true)
@@ -403,8 +406,14 @@ object Cobblemon {
val pokedexMongoFactory = CachedPlayerDataStoreFactory(DexDataMongoBackend(mongoClient, config.mongoDBDatabaseName, "PokeDexCollection"))
pokedexMongoFactory.setup(server)
+ val tmMovesMongoFactory = CachedPlayerDataStoreFactory(
+ TMMoveMongoBackend(mongoClient, config.mongoDBDatabaseName, "TMMovesCollection")
+ )
+ tmMovesMongoFactory.setup(server)
+
playerDataManager.setFactory(generalMongoFactory, PlayerInstancedDataStoreTypes.GENERAL)
playerDataManager.setFactory(pokedexMongoFactory, PlayerInstancedDataStoreTypes.POKEDEX)
+ playerDataManager.setFactory(tmMovesMongoFactory, PlayerInstancedDataStoreTypes.TM_MOVES)
MongoDBStoreAdapter(mongoClient, config.mongoDBDatabaseName)
} catch (e: ClassNotFoundException) {
LOGGER.error("MongoDB driver not found.")
@@ -588,6 +597,7 @@ object Cobblemon {
this.implementation.registerCommandArgument(cobblemonResource("mark"), MarkArgumentType::class, SingletonArgumentInfo.contextFree(MarkArgumentType::mark))
this.implementation.registerCommandArgument(cobblemonResource("transform_type"), TransformTypeArgumentType::class, SingletonArgumentInfo.contextFree(TransformTypeArgumentType::transformType))
this.implementation.registerCommandArgument(cobblemonResource("model_part"), ModelPartArgumentType::class, SingletonArgumentInfo.contextFree(ModelPartArgumentType::modelPart))
+ this.implementation.registerCommandArgument(cobblemonResource("tm"), TmArgumentType::class, SingletonArgumentInfo.contextFree(TmArgumentType::tm))
}
}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonBlockEntities.kt b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonBlockEntities.kt
index d6f751cb7d..bf3622c86c 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonBlockEntities.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonBlockEntities.kt
@@ -39,6 +39,9 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
@JvmField
val LECTERN: BlockEntityType<LecternBlockEntity> = this.create("lectern", BlockEntityType.Builder.of(::LecternBlockEntity, CobblemonBlocks.LECTERN).build(null))
+ @JvmField
+ val TM_SHELF: BlockEntityType<TMShelfBlockEntity> = this.create("tm_shelf", BlockEntityType.Builder.of(::TMShelfBlockEntity, CobblemonBlocks.TM_SHELF).build(null))
+
@JvmField
val BERRY = this.create("berry", BlockEntityType.Builder.of(::BerryBlockEntity, *CobblemonBlocks.berries().values.toTypedArray()).build(null))
@@ -48,6 +51,8 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
val SIGN: BlockEntityType<CobblemonSignBlockEntity> = this.create("sign", BlockEntityType.Builder.of(::CobblemonSignBlockEntity, CobblemonBlocks.APRICORN_SIGN, CobblemonBlocks.APRICORN_WALL_SIGN, CobblemonBlocks.SACCHARINE_SIGN, CobblemonBlocks.SACCHARINE_WALL_SIGN).build(null))
@JvmField
val HANGING_SIGN: BlockEntityType<CobblemonHangingSignBlockEntity> = this.create("hanging_sign", BlockEntityType.Builder.of(::CobblemonHangingSignBlockEntity, CobblemonBlocks.APRICORN_HANGING_SIGN, CobblemonBlocks.APRICORN_WALL_HANGING_SIGN, CobblemonBlocks.SACCHARINE_HANGING_SIGN, CobblemonBlocks.SACCHARINE_WALL_HANGING_SIGN).build(null))
+ @JvmField
+ val TM_MACHINE: BlockEntityType<TMMachineBlockEntity> = this.create("tm_machine", BlockEntityType.Builder.of(::TMMachineBlockEntity, CobblemonBlocks.TM_MACHINE).build(null))
@JvmField
val GILDED_CHEST: BlockEntityType<GildedChestBlockEntity> = this.create("chest", BlockEntityType.Builder.of(::GildedChestBlockEntity,
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonBlocks.kt b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonBlocks.kt
index c330a9c7dc..a185e7bf84 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonBlocks.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonBlocks.kt
@@ -22,6 +22,7 @@ import com.cobblemon.mod.common.block.sign.CobblemonHangingSignBlock
import com.cobblemon.mod.common.block.sign.CobblemonSignBlock
import com.cobblemon.mod.common.block.sign.CobblemonWallHangingSignBlock
import com.cobblemon.mod.common.block.sign.CobblemonWallSignBlock
+import com.cobblemon.mod.common.block.tmmachine.TMMachineBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationCarpetBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationalBlock
import com.cobblemon.mod.common.mixin.invoker.*
@@ -249,31 +250,31 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
val POKE_SNACK = this.create("poke_snack", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(BlockStateProperties.LIT)) 3 else 0 }, true))
@JvmField
- val TUMBLESTONE_CLUSTER = tumblestoneBlock("tumblestone_cluster", GrowableStoneBlock.STAGE_3, 7, 3, null)
+ val TUMBLESTONE_CLUSTER = tumblestoneBlock("tumblestone_cluster", TumblestoneBlock.STAGE_3, 7, 3, null)
@JvmField
- val LARGE_BUDDING_TUMBLESTONE = tumblestoneBlock("large_budding_tumblestone", GrowableStoneBlock.STAGE_2, 5, 3, TUMBLESTONE_CLUSTER)
+ val LARGE_BUDDING_TUMBLESTONE = tumblestoneBlock("large_budding_tumblestone", TumblestoneBlock.STAGE_2, 5, 3, TUMBLESTONE_CLUSTER)
@JvmField
- val MEDIUM_BUDDING_TUMBLESTONE = tumblestoneBlock("medium_budding_tumblestone", GrowableStoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_TUMBLESTONE)
+ val MEDIUM_BUDDING_TUMBLESTONE = tumblestoneBlock("medium_budding_tumblestone", TumblestoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_TUMBLESTONE)
@JvmField
- val SMALL_BUDDING_TUMBLESTONE = tumblestoneBlock("small_budding_tumblestone", GrowableStoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_TUMBLESTONE)
+ val SMALL_BUDDING_TUMBLESTONE = tumblestoneBlock("small_budding_tumblestone", TumblestoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_TUMBLESTONE)
@JvmField
- val SKY_TUMBLESTONE_CLUSTER = tumblestoneBlock("sky_tumblestone_cluster", GrowableStoneBlock.STAGE_3, 7, 3, null)
+ val SKY_TUMBLESTONE_CLUSTER = tumblestoneBlock("sky_tumblestone_cluster", TumblestoneBlock.STAGE_3, 7, 3, null)
@JvmField
- val LARGE_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("large_budding_sky_tumblestone", GrowableStoneBlock.STAGE_2, 5, 3, SKY_TUMBLESTONE_CLUSTER)
+ val LARGE_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("large_budding_sky_tumblestone", TumblestoneBlock.STAGE_2, 5, 3, SKY_TUMBLESTONE_CLUSTER)
@JvmField
- val MEDIUM_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("medium_budding_sky_tumblestone", GrowableStoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_SKY_TUMBLESTONE)
+ val MEDIUM_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("medium_budding_sky_tumblestone", TumblestoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_SKY_TUMBLESTONE)
@JvmField
- val SMALL_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("small_budding_sky_tumblestone", GrowableStoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_SKY_TUMBLESTONE)
+ val SMALL_BUDDING_SKY_TUMBLESTONE = tumblestoneBlock("small_budding_sky_tumblestone", TumblestoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_SKY_TUMBLESTONE)
@JvmField
- val BLACK_TUMBLESTONE_CLUSTER = tumblestoneBlock("black_tumblestone_cluster", GrowableStoneBlock.STAGE_3, 7, 3, null)
+ val BLACK_TUMBLESTONE_CLUSTER = tumblestoneBlock("black_tumblestone_cluster", TumblestoneBlock.STAGE_3, 7, 3, null)
@JvmField
- val LARGE_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("large_budding_black_tumblestone", GrowableStoneBlock.STAGE_2, 5, 3, BLACK_TUMBLESTONE_CLUSTER)
+ val LARGE_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("large_budding_black_tumblestone", TumblestoneBlock.STAGE_2, 5, 3, BLACK_TUMBLESTONE_CLUSTER)
@JvmField
- val MEDIUM_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("medium_budding_black_tumblestone", GrowableStoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_BLACK_TUMBLESTONE)
+ val MEDIUM_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("medium_budding_black_tumblestone", TumblestoneBlock.STAGE_1, 4, 3, LARGE_BUDDING_BLACK_TUMBLESTONE)
@JvmField
- val SMALL_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("small_budding_black_tumblestone", GrowableStoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_BLACK_TUMBLESTONE)
+ val SMALL_BUDDING_BLACK_TUMBLESTONE = tumblestoneBlock("small_budding_black_tumblestone", TumblestoneBlock.STAGE_0, 3, 4, MEDIUM_BUDDING_BLACK_TUMBLESTONE)
@JvmField
val TUMBLESTONE_BLOCK = this.create("tumblestone_block", Block(BlockBehaviour.Properties.of().mapColor(MapColor.TERRACOTTA_ORANGE).strength(1.0F).sound(CobblemonSounds.TUMBLESTONE_BLOCK_SOUNDS).requiresCorrectToolForDrops().instrument(NoteBlockInstrument.BASEDRUM)))
@@ -282,6 +283,87 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val BLACK_TUMBLESTONE_BLOCK = this.create("black_tumblestone_block", Block(BlockBehaviour.Properties.of().mapColor(MapColor.TERRACOTTA_BLACK).strength(1.0F).sound(CobblemonSounds.TUMBLESTONE_BLOCK_SOUNDS).requiresCorrectToolForDrops().instrument(NoteBlockInstrument.BASEDRUM)))
+ @JvmField
+ val TYPE_GEM_CORE = this.create("type_gem_core", TypeGemCoreBlock(BlockBehaviour.Properties.of().strength(4.0f, 10.0f).requiresCorrectToolForDrops()))
+
+ private val typeGemBlocks = mutableMapOf<ResourceLocation, TypeGemBlock>()
+
+ @JvmField
+ val TYPE_GEM_BLOCK_NORMAL = typeGemBlock("type_gem_block_normal")
+ @JvmField
+ val TYPE_GEM_BLOCK_FIRE = typeGemBlock("type_gem_block_fire")
+ @JvmField
+ val TYPE_GEM_BLOCK_WATER = typeGemBlock("type_gem_block_water")
+ @JvmField
+ val TYPE_GEM_BLOCK_ELECTRIC = typeGemBlock("type_gem_block_electric")
+ @JvmField
+ val TYPE_GEM_BLOCK_GRASS = typeGemBlock("type_gem_block_grass")
+ @JvmField
+ val TYPE_GEM_BLOCK_ICE = typeGemBlock("type_gem_block_ice")
+ @JvmField
+ val TYPE_GEM_BLOCK_FIGHTING = typeGemBlock("type_gem_block_fighting")
+ @JvmField
+ val TYPE_GEM_BLOCK_POISON = typeGemBlock("type_gem_block_poison")
+ @JvmField
+ val TYPE_GEM_BLOCK_GROUND = typeGemBlock("type_gem_block_ground")
+ @JvmField
+ val TYPE_GEM_BLOCK_FLYING = typeGemBlock("type_gem_block_flying")
+ @JvmField
+ val TYPE_GEM_BLOCK_PSYCHIC = typeGemBlock("type_gem_block_psychic")
+ @JvmField
+ val TYPE_GEM_BLOCK_BUG = typeGemBlock("type_gem_block_bug")
+ @JvmField
+ val TYPE_GEM_BLOCK_ROCK = typeGemBlock("type_gem_block_rock")
+ @JvmField
+ val TYPE_GEM_BLOCK_GHOST = typeGemBlock("type_gem_block_ghost")
+ @JvmField
+ val TYPE_GEM_BLOCK_DRAGON = typeGemBlock("type_gem_block_dragon")
+ @JvmField
+ val TYPE_GEM_BLOCK_DARK = typeGemBlock("type_gem_block_dark")
+ @JvmField
+ val TYPE_GEM_BLOCK_STEEL = typeGemBlock("type_gem_block_steel")
+ @JvmField
+ val TYPE_GEM_BLOCK_FAIRY = typeGemBlock("type_gem_block_fairy")
+
+ private val typeGemClusters = mutableMapOf<ResourceLocation, TypeGemClusterBlock>()
+
+ @JvmField
+ val TYPE_GEM_CLUSTER_NORMAL = typeGemCluster("type_gem_cluster_normal", TYPE_GEM_BLOCK_NORMAL, cobblemonResource("normal_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_FIRE = typeGemCluster("type_gem_cluster_fire", TYPE_GEM_BLOCK_FIRE, cobblemonResource("fire_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_WATER = typeGemCluster("type_gem_cluster_water", TYPE_GEM_BLOCK_WATER, cobblemonResource("water_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_ELECTRIC = typeGemCluster("type_gem_cluster_electric", TYPE_GEM_BLOCK_ELECTRIC, cobblemonResource("electric_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_GRASS = typeGemCluster("type_gem_cluster_grass", TYPE_GEM_BLOCK_GRASS, cobblemonResource("grass_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_ICE = typeGemCluster("type_gem_cluster_ice", TYPE_GEM_BLOCK_ICE, cobblemonResource("ice_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_FIGHTING = typeGemCluster("type_gem_cluster_fighting", TYPE_GEM_BLOCK_FIGHTING, cobblemonResource("fighting_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_POISON = typeGemCluster("type_gem_cluster_poison", TYPE_GEM_BLOCK_POISON, cobblemonResource("poison_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_GROUND = typeGemCluster("type_gem_cluster_ground", TYPE_GEM_BLOCK_GROUND, cobblemonResource("ground_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_FLYING = typeGemCluster("type_gem_cluster_flying", TYPE_GEM_BLOCK_FLYING, cobblemonResource("flying_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_PSYCHIC = typeGemCluster("type_gem_cluster_psychic", TYPE_GEM_BLOCK_PSYCHIC, cobblemonResource("psychic_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_BUG = typeGemCluster("type_gem_cluster_bug", TYPE_GEM_BLOCK_BUG, cobblemonResource("bug_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_ROCK = typeGemCluster("type_gem_cluster_rock", TYPE_GEM_BLOCK_ROCK, cobblemonResource("rock_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_GHOST = typeGemCluster("type_gem_cluster_ghost", TYPE_GEM_BLOCK_GHOST, cobblemonResource("ghost_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_DRAGON = typeGemCluster("type_gem_cluster_dragon", TYPE_GEM_BLOCK_DRAGON, cobblemonResource("dragon_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_DARK = typeGemCluster("type_gem_cluster_dark", TYPE_GEM_BLOCK_DARK, cobblemonResource("dark_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_STEEL = typeGemCluster("type_gem_cluster_steel", TYPE_GEM_BLOCK_STEEL, cobblemonResource("steel_gem"))
+ @JvmField
+ val TYPE_GEM_CLUSTER_FAIRY = typeGemCluster("type_gem_cluster_fairy", TYPE_GEM_BLOCK_FAIRY, cobblemonResource("fairy_gem"))
+
@JvmField
val POLISHED_TUMBLESTONE = this.create("polished_tumblestone", Block(BlockBehaviour.Properties.of().mapColor(MapColor.TERRACOTTA_ORANGE).strength(1.0F).sound(CobblemonSounds.TUMBLESTONE_BLOCK_SOUNDS).requiresCorrectToolForDrops().instrument(NoteBlockInstrument.BASEDRUM)))
@JvmField
@@ -456,6 +538,7 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val SOUL_CAMPFIRE = create("soul_campfire", CampfireBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.PODZOL).strength(2.0F).lightLevel{9}, true))
+
@JvmField
val RELIC_COIN_POUCH = create("relic_coin_pouch", CoinPouchBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.RELIC_COIN_POUCH_SOUNDS).pushReaction(PushReaction.DESTROY).noOcclusion(), true))
@JvmField
@@ -584,9 +667,25 @@ 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)) 13 else 0 }))
+ val TM_MACHINE = create(
+ "tm_machine",
+ TMMachineBlock(
+ BlockBehaviour.Properties.of()
+ .mapColor(MapColor.COLOR_GREEN)
+ .sound(SoundType.METAL)
+ .pushReaction(PushReaction.BLOCK)
+ .strength(2F)
+ .noOcclusion()
+ )
+ )
+
+
@JvmField
val LECTERN = create("lectern", LecternBlock(BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).instrument(NoteBlockInstrument.BASS).sound(SoundType.WOOD).strength(2.5F).ignitedByLava().lightLevel { if (it.getValue(LecternBlock.EMIT_LIGHT) as Boolean) 13 else 0 }))
+ @JvmField
+ val TM_SHELF = create("tm_shelf", TMShelfBlock(BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).instrument(NoteBlockInstrument.BASS).sound(SoundType.METAL).strength(2.5F)))
+
@JvmField
val DISPLAY_CASE = create("display_case", DisplayCaseBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.DISPLAY_CASE_SOUNDS).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.STONE).strength(0.3F).requiresCorrectToolForDrops()))
@@ -890,4 +989,28 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
val block = BlocksInvoker.createLeavesBlock(SoundType.GRASS)
return this.create(name, block)
}
+
+ private fun typeGemBlock(name: String): TypeGemBlock {
+ val typeGemBlock = this.create(name, TypeGemBlock(BlockBehaviour.Properties.of().strength(3.0f, 6.0f)))
+
+ val identifier = cobblemonResource(name)
+ typeGemBlocks[identifier] = typeGemBlock
+
+ return typeGemBlock
+ }
+
+ fun typeGemBlocks() = typeGemBlocks.toMap()
+
+ private fun typeGemCluster(name: String, gemBlock: Block, dropItemId: ResourceLocation): Block {
+ val gemClusterBlock = this.create(name, TypeGemClusterBlock(BlockBehaviour.Properties.of().strength(2.0f, 3.0f).pushReaction(PushReaction.DESTROY), gemBlock, dropItemId))
+
+ TypeGemClusterBlock.gemToClusterMap[gemBlock] = gemClusterBlock
+
+ val identifier = cobblemonResource(name)
+ typeGemClusters[identifier] = gemClusterBlock
+
+ return gemClusterBlock
+ }
+
+ fun typeGemClusters() = typeGemClusters.toMap()
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonCommands.kt b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonCommands.kt
index 095bb58e4f..7ba55575bf 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonCommands.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonCommands.kt
@@ -71,10 +71,12 @@ object CobblemonCommands {
MarkGiveAllCommand.register(dispatcher)
PcSearchCommand.register(dispatcher)
PcTakeCommand.register(dispatcher)
+ GiveTmCommand.register(dispatcher)
+ TmCommand.register(dispatcher)
// Possibly lock down registration if and only if under dev environment or running in an environment
// with a certain system environment variable set
CobblemonInfoCommand.register(dispatcher)
CalculateSeatPositionCommand.register(dispatcher)
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonImplementation.kt b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonImplementation.kt
index cafec5cb04..79c07adc50 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonImplementation.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonImplementation.kt
@@ -19,6 +19,7 @@ import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.server.packs.resources.PreparableReloadListener
import net.minecraft.tags.TagKey
+import net.minecraft.world.inventory.MenuType
import net.minecraft.world.level.GameRules
import net.minecraft.world.level.ItemLike
import net.minecraft.world.level.biome.Biome
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonItemComponents.kt b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonItemComponents.kt
index b3cf065118..bb3997bf27 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonItemComponents.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonItemComponents.kt
@@ -69,6 +69,12 @@ object CobblemonItemComponents : PlatformRegistry<Registry<DataComponentType<*>>
.networkSynchronized(FoodColourComponent.PACKET_CODEC)
.build())
+ @JvmField
+ val TM_MOVE: DataComponentType<TMMoveComponent> = create("tm_move", DataComponentType.builder<TMMoveComponent>()
+ .persistent(TMMoveComponent.CODEC)
+ .networkSynchronized(TMMoveComponent.PACKET_CODEC)
+ .build())
+
@JvmField
val INGREDIENT: DataComponentType<IngredientComponent> = create("ingredient", DataComponentType.builder<IngredientComponent>()
.persistent(IngredientComponent.CODEC)
@@ -101,6 +107,7 @@ object CobblemonItemComponents : PlatformRegistry<Registry<DataComponentType<*>>
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:flavour"), FLAVOUR)
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:ride_boosts"), RIDE_BOOST)
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:food_colour"), FOOD_COLOUR)
+ Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:tm_move"), TM_MOVE)
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:ingredient"), INGREDIENT)
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:food"), FOOD)
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:mob_effects"), MOB_EFFECTS)
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonItems.kt b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonItems.kt
index f086370495..3fedfb24d2 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonItems.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonItems.kt
@@ -457,6 +457,10 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val PC = blockItem("pc", CobblemonBlocks.PC)
@JvmField
+ val TM_MACHINE = blockItem("tm_machine", CobblemonBlocks.TM_MACHINE)
+ @JvmField
+ val TM_SHELF = blockItem("tm_shelf", CobblemonBlocks.TM_SHELF)
+ @JvmField
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
@JvmField
val DISPLAY_CASE = blockItem("display_case", CobblemonBlocks.DISPLAY_CASE)
@@ -1208,6 +1212,83 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val BLACK_TUMBLESTONE_BLOCK = blockItem("black_tumblestone_block", CobblemonBlocks.BLACK_TUMBLESTONE_BLOCK)
+ @JvmField
+ val TYPE_GEM_CORE = blockItem("type_gem_core", CobblemonBlocks.TYPE_GEM_CORE)
+
+ @JvmField
+ val TYPE_GEM_CLUSTER_NORMAL = blockItem("type_gem_cluster_normal", CobblemonBlocks.TYPE_GEM_CLUSTER_NORMAL)
+ @JvmField
+ val TYPE_GEM_CLUSTER_FIRE = blockItem("type_gem_cluster_fire", CobblemonBlocks.TYPE_GEM_CLUSTER_FIRE)
+ @JvmField
+ val TYPE_GEM_CLUSTER_WATER = blockItem("type_gem_cluster_water", CobblemonBlocks.TYPE_GEM_CLUSTER_WATER)
+ @JvmField
+ val TYPE_GEM_CLUSTER_ELECTRIC = blockItem("type_gem_cluster_electric", CobblemonBlocks.TYPE_GEM_CLUSTER_ELECTRIC)
+ @JvmField
+ val TYPE_GEM_CLUSTER_GRASS = blockItem("type_gem_cluster_grass", CobblemonBlocks.TYPE_GEM_CLUSTER_GRASS)
+ @JvmField
+ val TYPE_GEM_CLUSTER_ICE = blockItem("type_gem_cluster_ice", CobblemonBlocks.TYPE_GEM_CLUSTER_ICE)
+ @JvmField
+ val TYPE_GEM_CLUSTER_FIGHTING = blockItem("type_gem_cluster_fighting", CobblemonBlocks.TYPE_GEM_CLUSTER_FIGHTING)
+ @JvmField
+ val TYPE_GEM_CLUSTER_POISON = blockItem("type_gem_cluster_poison", CobblemonBlocks.TYPE_GEM_CLUSTER_POISON)
+ @JvmField
+ val TYPE_GEM_CLUSTER_GROUND = blockItem("type_gem_cluster_ground", CobblemonBlocks.TYPE_GEM_CLUSTER_GROUND)
+ @JvmField
+ val TYPE_GEM_CLUSTER_FLYING = blockItem("type_gem_cluster_flying", CobblemonBlocks.TYPE_GEM_CLUSTER_FLYING)
+ @JvmField
+ val TYPE_GEM_CLUSTER_PSYCHIC = blockItem("type_gem_cluster_psychic", CobblemonBlocks.TYPE_GEM_CLUSTER_PSYCHIC)
+ @JvmField
+ val TYPE_GEM_CLUSTER_BUG = blockItem("type_gem_cluster_bug", CobblemonBlocks.TYPE_GEM_CLUSTER_BUG)
+ @JvmField
+ val TYPE_GEM_CLUSTER_ROCK = blockItem("type_gem_cluster_rock", CobblemonBlocks.TYPE_GEM_CLUSTER_ROCK)
+ @JvmField
+ val TYPE_GEM_CLUSTER_GHOST = blockItem("type_gem_cluster_ghost", CobblemonBlocks.TYPE_GEM_CLUSTER_GHOST)
+ @JvmField
+ val TYPE_GEM_CLUSTER_DRAGON = blockItem("type_gem_cluster_dragon", CobblemonBlocks.TYPE_GEM_CLUSTER_DRAGON)
+ @JvmField
+ val TYPE_GEM_CLUSTER_DARK = blockItem("type_gem_cluster_dark", CobblemonBlocks.TYPE_GEM_CLUSTER_DARK)
+ @JvmField
+ val TYPE_GEM_CLUSTER_STEEL = blockItem("type_gem_cluster_steel", CobblemonBlocks.TYPE_GEM_CLUSTER_STEEL)
+ @JvmField
+ val TYPE_GEM_CLUSTER_FAIRY = blockItem("type_gem_cluster_fairy", CobblemonBlocks.TYPE_GEM_CLUSTER_FAIRY)
+
+ @JvmField
+ val TYPE_GEM_BLOCK_NORMAL = blockItem("type_gem_block_normal", CobblemonBlocks.TYPE_GEM_BLOCK_NORMAL)
+ @JvmField
+ val TYPE_GEM_BLOCK_FIRE = blockItem("type_gem_block_fire", CobblemonBlocks.TYPE_GEM_BLOCK_FIRE)
+ @JvmField
+ val TYPE_GEM_BLOCK_WATER = blockItem("type_gem_block_water", CobblemonBlocks.TYPE_GEM_BLOCK_WATER)
+ @JvmField
+ val TYPE_GEM_BLOCK_ELECTRIC = blockItem("type_gem_block_electric", CobblemonBlocks.TYPE_GEM_BLOCK_ELECTRIC)
+ @JvmField
+ val TYPE_GEM_BLOCK_GRASS = blockItem("type_gem_block_grass", CobblemonBlocks.TYPE_GEM_BLOCK_GRASS)
+ @JvmField
+ val TYPE_GEM_BLOCK_ICE = blockItem("type_gem_block_ice", CobblemonBlocks.TYPE_GEM_BLOCK_ICE)
+ @JvmField
+ val TYPE_GEM_BLOCK_FIGHTING = blockItem("type_gem_block_fighting", CobblemonBlocks.TYPE_GEM_BLOCK_FIGHTING)
+ @JvmField
+ val TYPE_GEM_BLOCK_POISON = blockItem("type_gem_block_poison", CobblemonBlocks.TYPE_GEM_BLOCK_POISON)
+ @JvmField
+ val TYPE_GEM_BLOCK_GROUND = blockItem("type_gem_block_ground", CobblemonBlocks.TYPE_GEM_BLOCK_GROUND)
+ @JvmField
+ val TYPE_GEM_BLOCK_FLYING = blockItem("type_gem_block_flying", CobblemonBlocks.TYPE_GEM_BLOCK_FLYING)
+ @JvmField
+ val TYPE_GEM_BLOCK_PSYCHIC = blockItem("type_gem_block_psychic", CobblemonBlocks.TYPE_GEM_BLOCK_PSYCHIC)
+ @JvmField
+ val TYPE_GEM_BLOCK_BUG = blockItem("type_gem_block_bug", CobblemonBlocks.TYPE_GEM_BLOCK_BUG)
+ @JvmField
+ val TYPE_GEM_BLOCK_ROCK = blockItem("type_gem_block_rock", CobblemonBlocks.TYPE_GEM_BLOCK_ROCK)
+ @JvmField
+ val TYPE_GEM_BLOCK_GHOST = blockItem("type_gem_block_ghost", CobblemonBlocks.TYPE_GEM_BLOCK_GHOST)
+ @JvmField
+ val TYPE_GEM_BLOCK_DRAGON = blockItem("type_gem_block_dragon", CobblemonBlocks.TYPE_GEM_BLOCK_DRAGON)
+ @JvmField
+ val TYPE_GEM_BLOCK_DARK = blockItem("type_gem_block_dark", CobblemonBlocks.TYPE_GEM_BLOCK_DARK)
+ @JvmField
+ val TYPE_GEM_BLOCK_STEEL = blockItem("type_gem_block_steel", CobblemonBlocks.TYPE_GEM_BLOCK_STEEL)
+ @JvmField
+ val TYPE_GEM_BLOCK_FAIRY = blockItem("type_gem_block_fairy", CobblemonBlocks.TYPE_GEM_BLOCK_FAIRY)
+
@JvmField
val POKEROD_SMITHING_TEMPLATE = create("pokerod_smithing_template", SmithingTemplateItem(
Component.translatable("item.minecraft.fishing_rod").blue(),
@@ -1432,44 +1513,49 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
val RELIC_COIN_POUCH = blockItem("relic_coin_pouch", CobblemonBlocks.RELIC_COIN_POUCH)
@JvmField
val RELIC_COIN_SACK = blockItem("relic_coin_sack", CobblemonBlocks.RELIC_COIN_SACK)
+ @JvmField
+ val TECHNICAL_MACHINE = this.create("technical_machine", TechnicalMachineItem(Properties()))
+ @JvmField
+ val BLANK_TM = this.create("blank_tm", CobblemonItem(Item.Properties()))
// Type Gems
@JvmField
- val NORMAL_GEM = noSettingsItem("normal_gem")
+ val NORMAL_GEM = this.create("normal_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_NORMAL, Item.Properties()))
@JvmField
- val FIRE_GEM = noSettingsItem("fire_gem")
+ val FIRE_GEM = this.create("fire_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_FIRE, Item.Properties()))
@JvmField
- val WATER_GEM = noSettingsItem("water_gem")
+ val WATER_GEM = this.create("water_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_WATER, Item.Properties()))
@JvmField
- val GRASS_GEM = noSettingsItem("grass_gem")
+ val GRASS_GEM = this.create("grass_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_GRASS, Item.Properties()))
@JvmField
- val ELECTRIC_GEM = noSettingsItem("electric_gem")
+ val ELECTRIC_GEM = this.create("electric_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_ELECTRIC, Item.Properties()))
@JvmField
- val ICE_GEM = noSettingsItem("ice_gem")
+ val ICE_GEM = this.create("ice_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_ICE, Item.Properties()))
@JvmField
- val FIGHTING_GEM = noSettingsItem("fighting_gem")
+ val FIGHTING_GEM = this.create("fighting_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_FIGHTING, Item.Properties()))
@JvmField
- val POISON_GEM = noSettingsItem("poison_gem")
+ val POISON_GEM = this.create("poison_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_POISON, Item.Properties()))
@JvmField
- val GROUND_GEM = noSettingsItem("ground_gem")
+ val GROUND_GEM = this.create("ground_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_GROUND, Item.Properties()))
@JvmField
- val FLYING_GEM = noSettingsItem("flying_gem")
+ val FLYING_GEM = this.create("flying_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_FLYING, Item.Properties()))
@JvmField
- val PSYCHIC_GEM = noSettingsItem("psychic_gem")
+ val PSYCHIC_GEM = this.create("psychic_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_PSYCHIC, Item.Properties()))
@JvmField
- val BUG_GEM = noSettingsItem("bug_gem")
+ val BUG_GEM = this.create("bug_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_BUG, Item.Properties()))
@JvmField
- val ROCK_GEM = noSettingsItem("rock_gem")
+ val ROCK_GEM = this.create("rock_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_ROCK, Item.Properties()))
@JvmField
- val GHOST_GEM = noSettingsItem("ghost_gem")
+ val GHOST_GEM = this.create("ghost_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_GHOST, Item.Properties()))
@JvmField
- val DRAGON_GEM = noSettingsItem("dragon_gem")
+ val DRAGON_GEM = this.create("dragon_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_DRAGON, Item.Properties()))
@JvmField
- val DARK_GEM = noSettingsItem("dark_gem")
+ val DARK_GEM = this.create("dark_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_DARK, Item.Properties()))
@JvmField
- val STEEL_GEM = noSettingsItem("steel_gem")
+ val STEEL_GEM = this.create("steel_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_STEEL, Item.Properties()))
@JvmField
- val FAIRY_GEM = noSettingsItem("fairy_gem")
+ val FAIRY_GEM = this.create("fairy_gem", TypeGemItem(CobblemonBlocks.TYPE_GEM_CLUSTER_FAIRY, Item.Properties()))
+
//@JvmField
//val BINDING_SOIL = blockItem("binding_soil", CobblemonBlocks.BINDING_SOIL)
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonMenuType.kt b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonMenuType.kt
index 894732b7eb..5c0c7b9ba7 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonMenuType.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonMenuType.kt
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common
import com.cobblemon.mod.common.block.campfirepot.CookingPotMenu
+import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
import com.cobblemon.mod.common.platform.PlatformRegistry
import net.minecraft.core.Registry
import net.minecraft.core.registries.BuiltInRegistries
@@ -19,7 +20,8 @@ import net.minecraft.world.inventory.MenuType
object CobblemonMenuType : PlatformRegistry<Registry<MenuType<*>>, ResourceKey<Registry<MenuType<*>>>, MenuType<*>>() {
- val COOKING_POT = MenuType.register("cooking_pot", ::CookingPotMenu)
+ val COOKING_POT = MenuType.register("cooking_pot_menu", ::CookingPotMenu)
+ val TM_MACHINE = MenuType.register("tm_machine_menu", ::TMMachineMenu)
override val registry: Registry<MenuType<*>>
get() = BuiltInRegistries.MENU
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonNetwork.kt b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonNetwork.kt
index 8f7fec94ee..f33b84a9a8 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonNetwork.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonNetwork.kt
@@ -25,15 +25,15 @@ import com.cobblemon.mod.common.client.net.data.RideSettingsSyncHandler
import com.cobblemon.mod.common.client.net.debug.OpenRidingStatsDebugGUIHandler
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.*
+import com.cobblemon.mod.common.client.net.gui.*
import com.cobblemon.mod.common.client.net.effect.PokeSnackBlockParticlesHandler
import com.cobblemon.mod.common.client.net.effect.RunPosableMoLangHandler
import com.cobblemon.mod.common.client.net.effect.SaccharineLogBlockParticlesHandler
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.ExpGainedDataPacketHandler
-import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
-import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
-import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
+import com.cobblemon.mod.common.client.net.gui.*
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
@@ -137,9 +137,7 @@ import com.cobblemon.mod.common.net.messages.client.trade.TradeProcessStartedPac
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeUpdatedPacket
import com.cobblemon.mod.common.net.messages.client.ui.ExpGainedDataPacket
-import com.cobblemon.mod.common.net.messages.client.ui.InteractPokemonUIPacket
-import com.cobblemon.mod.common.net.messages.client.ui.PokedexUIPacket
-import com.cobblemon.mod.common.net.messages.client.ui.SummaryUIPacket
+import com.cobblemon.mod.common.net.messages.client.ui.*
import com.cobblemon.mod.common.net.messages.server.*
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
@@ -150,6 +148,7 @@ import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.net.messages.server.behaviour.DamageOnCollisionPacket
import com.cobblemon.mod.common.net.messages.server.behaviour.SetEntityBehaviourPacket
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
+import com.cobblemon.mod.common.net.messages.server.block.TMMachineTeachMovePacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.callback.party.PartyPokemonSelectedPacket
@@ -205,6 +204,7 @@ import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestResponseHan
import com.cobblemon.mod.common.net.serverhandling.behaviour.DamageOnCollisionPacketHandler
import com.cobblemon.mod.common.net.serverhandling.behaviour.SetEntityBehaviourHandler
import com.cobblemon.mod.common.net.serverhandling.block.AdjustBlockEntityViewerCountHandler
+import com.cobblemon.mod.common.net.serverhandling.block.TMMachineTeachMoveHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.callback.party.PartyPokemonSelectedHandler
@@ -228,6 +228,8 @@ import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandl
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.StartScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
+import com.cobblemon.mod.common.net.serverhandling.pokemon.update.*
+import com.cobblemon.mod.common.net.serverhandling.pokemon.update.*
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetItemHiddenHandler
@@ -570,6 +572,7 @@ object CobblemonNetwork {
// Block entity
list.add(PacketRegisterInfo(AdjustBlockEntityViewerCountPacket.ID, AdjustBlockEntityViewerCountPacket::decode, AdjustBlockEntityViewerCountHandler))
+ list.add(PacketRegisterInfo(TMMachineTeachMovePacket.ID, TMMachineTeachMovePacket::decode, TMMachineTeachMoveHandler))
// Move select packets
list.add(PacketRegisterInfo(MoveSelectedPacket.ID, MoveSelectedPacket::decode, MoveSelectedHandler))
@@ -606,6 +609,10 @@ object CobblemonNetwork {
// Cooking
list.add(PacketRegisterInfo(ToggleCookingPotLidPacket.ID, ToggleCookingPotLidPacket::decode, ToggleCookingPotLidHandler))
+ // TM Machine
+ list.add(PacketRegisterInfo(SetActiveTMPacket.ID, SetActiveTMPacket::decode, SetActiveTMPacketHandler))
+ list.add(PacketRegisterInfo(SetTMMachineContainerDataPacket.ID, SetTMMachineContainerDataPacket::decode, SetTMMachineContainerDataPacketHandler))
+
// Behaviour Packets
list.add(PacketRegisterInfo(SetEntityBehaviourPacket.ID, SetEntityBehaviourPacket::decode, SetEntityBehaviourHandler))
list.add(PacketRegisterInfo(DamageOnCollisionPacket.ID, DamageOnCollisionPacket::decode, DamageOnCollisionPacketHandler))
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonSounds.kt b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonSounds.kt
index 7faf143251..eb1c789f08 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonSounds.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/CobblemonSounds.kt
@@ -200,6 +200,22 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
@JvmField
val TUMBLESTONE_STEP = this.create("block.tumblestone.step")
+ @JvmField
+ val TM_USE = this.create("item.tm.use")
+ @JvmField
+ val TM_MACHINE_CRAFT = this.create("block.tm_machine.craft")
+ @JvmField
+ val TM_MACHINE_OFF = this.create("block.tm_machine.off")
+ @JvmField
+ val TM_MACHINE_ON = this.create("block.tm_machine.on")
+ @JvmField
+ val TM_MACHINE_OPEN = this.create("block.tm_machine.open")
+ @JvmField
+ val TM_MACHINE_CLOSE = this.create("block.tm_machine.close")
+ @JvmField
+ val TM_MACHINE_PLACE_DISC = this.create("block.tm_machine.place_disc")
+
+
@JvmField
val EVOLUTION_STONE_BLOCK_BREAK = this.create("block.evolution_stone_block.break")
@JvmField
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/advancement/criterion/PlantTumblestoneCriterionCondition.kt b/common/src/main/kotlin/com/cobblemon/mod/common/advancement/criterion/PlantTumblestoneCriterionCondition.kt
index ee266cfd9b..af2d0dbcc6 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/advancement/criterion/PlantTumblestoneCriterionCondition.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/advancement/criterion/PlantTumblestoneCriterionCondition.kt
@@ -14,7 +14,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.advancements.critereon.ContextAwarePredicate
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerPlayer
-import java.util.Optional
+import java.util.*
class PlantTumblestoneContext(val pos: BlockPos, val block: TumblestoneBlock)
@@ -29,42 +29,6 @@ class PlantTumblestoneCriterion(
}
override fun matches(player: ServerPlayer, context: PlantTumblestoneContext): Boolean {
- return context.block.canGrow(context.pos, player.level())
+ return context.block.canGrow(player.level().getBlockState(context.pos), context.pos, player.level())
}
-}
-
-///**
-// * A criterion condition for when a tumblestone block is planted
-// * @param id The identifier of the criterion
-// * @param predicate The predicate for the criterion
-// *
-// * @author Aethen
-// * @since 02/28/2024
-// */
-//class PlantTumblestoneCriterionCondition(id: Identifier, predicate: LootContextPredicate) :
-// SimpleCriterionCondition<PlantTumblestoneContext>(id, predicate) {
-// override fun toJson(json: JsonObject) {
-// // Add properties to json if needed. None needed for this criterion
-// }
-//
-// override fun fromJson(json: JsonObject) {
-// // Parse properties from json if needed. None needed for this criterion
-// }
-//
-// /**
-// * Checks if the tumblestone block can grow at the given position
-// * @param player The player that planted the tumblestone block
-// * @param context The context of the criterion
-// * @return True if the tumblestone block can grow at the given position, false otherwise
-// */
-// override fun matches(player: ServerPlayer, context: PlantTumblestoneContext): Boolean {
-// return context.tumbleStoneBlock.canGrow(context.pos, player.level())
-// }
-//}
-//
-///**
-// * The context of the [PlantTumblestoneCriterionCondition]
-// * @param pos The position of the tumblestone block
-// * @param tumbleStoneBlock The tumblestone block
-// */
-//open class PlantTumblestoneContext(var pos: BlockPos, var tumbleStoneBlock: TumblestoneBlock)
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/moves/BenchedMove.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/moves/BenchedMove.kt
index 377fc7c55e..73a0fd25d4 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/moves/BenchedMove.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/moves/BenchedMove.kt
@@ -24,6 +24,7 @@ import net.minecraft.network.RegistryFriendlyByteBuf
class BenchedMoves : Iterable<BenchedMove> {
var changeFunction: ((BenchedMoves) -> Unit) = {}
+// var moveAddedFunction: (BenchedMove) -> Unit = {}
private var emit = true
private val benchedMoves = mutableListOf<BenchedMove>()
@@ -58,6 +59,9 @@ class BenchedMoves : Iterable<BenchedMove> {
return false
}
+ /*if (emit) {
+ moveAddedFunction(benchedMove)
+ }*/
doThenEmit { benchedMoves.add(benchedMove) }
return true
}
@@ -174,4 +178,4 @@ data class BenchedMove(val moveTemplate: MoveTemplate, val ppRaisedStages: Int)
Codec.intRange(0, 3).fieldOf(DataKeys.POKEMON_MOVESET_RAISED_PP_STAGES).forGetter(BenchedMove::ppRaisedStages)
).apply(it, ::BenchedMove) }
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/moves/MoveSet.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/moves/MoveSet.kt
index a3d7a9b731..1ce93ea017 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/moves/MoveSet.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/moves/MoveSet.kt
@@ -54,6 +54,7 @@ class MoveSet : Iterable<Move> {
*/
fun getMoves() = moves.filterNotNull()
fun getMovesWithNulls() = moves.toList()
+ fun getMoveTemplates() = getMoves().map { move -> move.template }
fun hasSpace() = moves.any { it == null }
/**
@@ -222,4 +223,4 @@ class MoveSet : Iterable<Move> {
{ moveSet -> moveSet.toList() }
)
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/permission/CobblemonPermissions.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/permission/CobblemonPermissions.kt
index 3d0bff1e6c..3022fef269 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/permission/CobblemonPermissions.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/permission/CobblemonPermissions.kt
@@ -143,6 +143,8 @@ object CobblemonPermissions {
val CHANGE_BOX_COUNT = this.create("${COMMAND_PREFIX}boxcount", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val SPECTATE_BATTLE = this.create("${COMMAND_PREFIX}spectatebattle", PermissionLevel.ALL_COMMANDS)
+ @JvmStatic
+ val TECHNICAL_MACHINE = this.create("${COMMAND_PREFIX}technicalmachine", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val SEE_HIDDEN_NPCS = this.create("seehiddennpcs", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokeball/PokeBalls.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokeball/PokeBalls.kt
index cc6d681722..c7b926d1c9 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokeball/PokeBalls.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokeball/PokeBalls.kt
@@ -289,11 +289,11 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
createDefault("ancient_great_ball", MultiplierModifier(1.5F), ancient = true)
createDefault("ancient_ultra_ball", MultiplierModifier(2F), ancient = true)
createDefault("ancient_heavy_ball", throwPower = 0.75f, ancient = true)
- createDefault("ancient_leaden_ball", MultiplierModifier(1.5F), throwPower = 0.75f, ancient = true)
- createDefault("ancient_gigaton_ball", MultiplierModifier(2F), throwPower = 0.75f, ancient = true)
+ createDefault("ancient_leaden_ball", throwPower = 0.75f, ancient = true)
+ createDefault("ancient_gigaton_ball", throwPower = 0.75f, ancient = true)
createDefault("ancient_feather_ball", throwPower = 2.5f, ancient = true)
- createDefault("ancient_wing_ball", MultiplierModifier(1.5F), throwPower = 2.5f, ancient = true)
- createDefault("ancient_jet_ball", MultiplierModifier(2F), throwPower = 2.5f, ancient = true)
+ createDefault("ancient_wing_ball", throwPower = 2.5f, ancient = true)
+ createDefault("ancient_jet_ball", throwPower = 2.5f, ancient = true)
createDefault("ancient_origin_ball", GuaranteedModifier(), ancient = true)
// Luxury ball effect, low priority as it must be triggered before soothe bell as of gen 4
CobblemonEvents.FRIENDSHIP_UPDATED.subscribe(priority = Priority.LOW) { event ->
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/AbstractPokedexManager.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/AbstractPokedexManager.kt
index 0b5bfe46d7..aa55d4056b 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/AbstractPokedexManager.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/AbstractPokedexManager.kt
@@ -29,7 +29,6 @@ abstract class AbstractPokedexManager {
fun deleteSpeciesRecord(speciesId: ResourceLocation) {
speciesRecords.remove(speciesId)
- markDirty()
}
fun deleteFormRecord(speciesId: ResourceLocation, formName: String) {
@@ -40,8 +39,6 @@ abstract class AbstractPokedexManager {
deleteSpeciesRecord(speciesId)
return
}
-
- markDirty()
}
fun getSpeciesRecord(speciesId: ResourceLocation): SpeciesDexRecord? {
@@ -164,8 +161,4 @@ abstract class AbstractPokedexManager {
return newValue
}
}
-
- open fun markDirty() {
- // Save stuff
- }
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/FormDexRecord.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/FormDexRecord.kt
index 508a7f77c5..d0334c26f3 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/FormDexRecord.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/FormDexRecord.kt
@@ -10,7 +10,9 @@ package com.cobblemon.mod.common.api.pokedex
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.struct.VariableStruct
+import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
+import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.pokemon.PokedexDataChangedEvent
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
@@ -25,6 +27,7 @@ import com.google.common.collect.Sets
import com.mojang.datafixers.util.Either
import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.ListCodec
+import com.mojang.serialization.codecs.PrimitiveCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.network.RegistryFriendlyByteBuf
@@ -41,11 +44,13 @@ class FormDexRecord {
instance.group(
ListCodec(Codec.STRING, 0, 3).fieldOf("genders").forGetter { it.genders.map { it.name } },
ListCodec(Codec.STRING, 0, 2).fieldOf("seenShinyStates").forGetter { it.seenShinyStates.toList() },
+ PrimitiveCodec.INT.optionalFieldOf("highest_level", -1).forGetter { it.highestLevel },
Codec.STRING.fieldOf("knowledge").forGetter { it.knowledge.name }
- ).apply(instance) { genders, seenShinyStates, knowledge ->
+ ).apply(instance) { genders, seenShinyStates, highestLevel, knowledge ->
FormDexRecord().also {
it.genders.addAll(genders.map(Gender::valueOf))
it.seenShinyStates.addAll(seenShinyStates)
+ it.highestLevel = highestLevel
it.knowledge = PokedexEntryProgress.valueOf(knowledge)
}
}
@@ -59,6 +64,9 @@ class FormDexRecord {
private val seenShinyStates =
mutableSetOf<String>() // consider: radiants in the future (radiants should just be a resource pack tbh)
+ var highestLevel = -1
+ private set
+
/** The current awareness of the form that the dex has. */
var knowledge = PokedexEntryProgress.NONE
private set
@@ -80,11 +88,17 @@ class FormDexRecord {
struct = QueryStruct(hashMapOf())
.addFunction("data") { data }
.addFunction("knowledge") { StringValue(knowledge.name) }
- .addFunction("has_seen_gender") { params -> genders.contains(Gender.valueOf(params.getString(0).uppercase())) }
+ .addFunction("has_seen_gender") { params -> DoubleValue(genders.contains(Gender.valueOf(params.getString(0).uppercase()))) }
+ .addFunction("highest_level") { _ -> DoubleValue(highestLevel) }
+ .addFunction("set_highest_level") { params ->
+ highestLevel = maxOf(highestLevel, params.getDouble(0).toInt())
+ DoubleValue.ONE
+ }
}
fun clone() = FormDexRecord().also {
it.genders.addAll(genders)
+ it.highestLevel = highestLevel
it.seenShinyStates.addAll(seenShinyStates)
it.knowledge = knowledge
}
@@ -114,7 +128,7 @@ class FormDexRecord {
fun addAllShinyStatesAndGenders() {
val form = PokemonSpecies.getByIdentifier(speciesDexRecord.id)?.getFormByName(formName)
genders.addAll(form?.possibleGenders ?: listOf(Gender.MALE, Gender.FEMALE))
-
+ highestLevel = Cobblemon.config.maxPokemonLevel
seenShinyStates.addAll(listOf("shiny", "normal"))
speciesDexRecord.onFormRecordUpdated(this)
}
@@ -135,6 +149,9 @@ class FormDexRecord {
),
ifSucceeded = {
genders.add(pokedexEntityData.pokemon.gender)
+ if (knowledge == PokedexEntryProgress.CAUGHT) {
+ highestLevel = maxOf(highestLevel, pokedexEntityData.pokemon.level)
+ }
seenShinyStates.add(if (pokedexEntityData.pokemon.shiny) "shiny" else "normal")
if (knowledge.ordinal > this.knowledge.ordinal) {
this.knowledge = knowledge
@@ -159,6 +176,7 @@ class FormDexRecord {
|| (pokedexEntityData.pokemon.shiny && "shiny" !in seenShinyStates)
|| (!pokedexEntityData.pokemon.shiny && "normal" !in seenShinyStates)
|| knowledge.ordinal > this.knowledge.ordinal
+ || (highestLevel < pokedexEntityData.pokemon.level && knowledge == PokedexEntryProgress.CAUGHT)
|| speciesDexRecord.wouldBeDifferent(pokedexEntityData)
}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/PokedexManager.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/PokedexManager.kt
index b2488918b1..bd0062a506 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/PokedexManager.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/PokedexManager.kt
@@ -9,6 +9,8 @@
package com.cobblemon.mod.common.api.pokedex
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
+import com.cobblemon.mod.common.api.scheduling.ScheduledTask
+import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
import com.cobblemon.mod.common.api.storage.player.InstancedPlayerData
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
@@ -48,10 +50,44 @@ class PokedexManager(
fun obtain(pokemon: Pokemon) {
val speciesId = pokemon.species.resourceIdentifier
val formName = pokemon.form.name
- getOrCreateSpeciesRecord(speciesId).getOrCreateFormRecord(formName).caught(PokedexEntityData(pokemon = pokemon, disguise = null))
+ getOrCreateSpeciesRecord(speciesId).getOrCreateFormRecord(formName).obtained(PokedexEntityData(pokemon = pokemon, disguise = null))
}
- override fun markDirty() {
+ fun scheduleFullSyncFromStores(
+ party: Iterable<Pokemon?>,
+ pc: Iterable<Pokemon?>,
+ batchSize: Int = 50
+ ) {
+ val iterator = sequence {
+ for (pokemon in party) {
+ if (pokemon != null) yield(pokemon)
+ }
+ for (pokemon in pc) {
+ if (pokemon != null) yield(pokemon)
+ }
+ }.iterator()
+
+ if (!iterator.hasNext()) {
+ return
+ }
+
+ ScheduledTask.Builder()
+ .tracker(ServerTaskTracker)
+ .interval(0F)
+ .infiniteIterations()
+ .execute { task ->
+ var processed = 0
+ while (processed < batchSize && iterator.hasNext()) {
+ val pokemon = iterator.next()
+ obtain(pokemon)
+ processed++
+ }
+
+ if (!iterator.hasNext()) {
+ task.expire()
+ }
+ }
+ .build()
}
override fun initialize() {
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/SpeciesDexRecord.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/SpeciesDexRecord.kt
index 639c98391f..90e31aeace 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/SpeciesDexRecord.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/SpeciesDexRecord.kt
@@ -26,7 +26,8 @@ import net.minecraft.resources.ResourceLocation
/**
* Dex recorded information about a particular Pokémon species. This includes all forms and their information.
* Across all forms there is a single list of known aspects which is generally used to track cosmetic variation
- * though technically things like gender and form-motivating aspects will get collected inadvertently.
+ * though technically things like gender and form-motivating aspects will get collected inadvertently. The highest
+ * level of the species that the player has owned is also tracked.
*
* @author Hiroku
* @since August 23rd, 2024
@@ -36,10 +37,12 @@ class SpeciesDexRecord {
val CODEC: Codec<SpeciesDexRecord> = RecordCodecBuilder.create { instance ->
instance.group(
ListCodec(PrimitiveCodec.STRING, 0, 512).fieldOf("aspects").forGetter { it.aspects.toList() },
+ PrimitiveCodec.INT.optionalFieldOf("highestLevel", -1).forGetter { it.highestLevel },
Codec.unboundedMap(Codec.STRING, FormDexRecord.CODEC).fieldOf("formRecords").forGetter { it.formRecords }
- ).apply(instance) { aspects, formRecords ->
+ ).apply(instance) { aspects, highestLevel, formRecords ->
SpeciesDexRecord().also {
it.aspects.addAll(aspects)
+ it.highestLevel = highestLevel
it.formRecords.putAll(formRecords)
}
}
@@ -48,13 +51,16 @@ class SpeciesDexRecord {
@Transient
lateinit var id: ResourceLocation
+ /** The highest level of the species that the player has owned. */
+ var highestLevel = -1
+ private set
private val aspects: MutableSet<String> = mutableSetOf()
private val formRecords: MutableMap<String, FormDexRecord> = mutableMapOf()
val isFormRecordsEmpty: Boolean
get() = formRecords.isEmpty()
fun describe(): String {
- return "SpeciesDexRecord(aspects=$aspects, formRecords=$formRecords)"
+ return "SpeciesDexRecord(aspects=$aspects, highestLevel=$highestLevel, formRecords=$formRecords)"
}
@Transient
@@ -77,7 +83,14 @@ class SpeciesDexRecord {
.addFunction("is_caught") { params ->
DoubleValue(formRecords.values.any { it.knowledge.name.equals("CAUGHT", ignoreCase = true) })
}
- // more, also worth moving to something like MoLangFunctions.kt so it's easier to API in more values
+ .addFunction("highest_level") { params ->
+ DoubleValue(highestLevel)
+ }
+ .addFunction("set_highest_level") { params ->
+ highestLevel = params.getDouble(0).toInt()
+ DoubleValue.ONE
+ }
+ // more, also worth moving to something like MoLangFunctions.kt so it's easier to API in more values
@Transient
lateinit var pokedexManager: AbstractPokedexManager
@@ -94,10 +107,16 @@ class SpeciesDexRecord {
fun addInformation(pokemon: Pokemon, knowledge: PokedexEntryProgress) {
aspects.addAll(pokemon.aspects)
+ if (knowledge == PokedexEntryProgress.CAUGHT) {
+ highestLevel = maxOf(highestLevel, pokemon.level)
+ }
}
fun addInformation(pokedexEntityData: PokedexEntityData, knowledge: PokedexEntryProgress) {
aspects.addAll(pokedexEntityData.pokemon.aspects)
+ if (knowledge == PokedexEntryProgress.CAUGHT) {
+ highestLevel = maxOf(highestLevel, pokedexEntityData.pokemon.level)
+ }
}
fun addAspects(addedAspects: Set<String>) {
@@ -128,6 +147,7 @@ class SpeciesDexRecord {
fun clone() = SpeciesDexRecord().also {
it.aspects.addAll(aspects)
+ it.highestLevel = highestLevel
it.formRecords.putAll(formRecords.mapValues { it.value.clone() })
}
@@ -139,6 +159,7 @@ class SpeciesDexRecord {
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeCollection(aspects) { _, it -> buffer.writeString(it) }
+ buffer.writeInt(highestLevel)
buffer.writeInt(formRecords.size)
for ((formName, formRecord) in formRecords) {
buffer.writeString(formName)
@@ -149,6 +170,7 @@ class SpeciesDexRecord {
fun decode(buffer: RegistryFriendlyByteBuf) {
aspects.clear()
aspects.addAll(buffer.readCollection(Sets::newHashSetWithExpectedSize) { buffer.readString() })
+ highestLevel = buffer.readInt()
formRecords.clear()
val numForms = buffer.readInt()
for (i in 0 until numForms) {
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SearchByType.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SearchByType.kt
index b4352d6902..c28abea392 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SearchByType.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SearchByType.kt
@@ -11,5 +11,6 @@ package com.cobblemon.mod.common.api.pokedex.filter
enum class SearchByType {
SPECIES,
ABILITIES,
+ MOVES,
DROPS;
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SearchFilter.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SearchFilter.kt
index 5bcd6d2744..8f46f93f35 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SearchFilter.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SearchFilter.kt
@@ -9,14 +9,22 @@
package com.cobblemon.mod.common.api.pokedex.filter
import com.cobblemon.mod.common.api.drop.ItemDropEntry
+import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.pokedex.AbstractPokedexManager
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
+import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
+import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
+import com.cobblemon.mod.common.pokemon.FormData
+import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.itemRegistry
import net.minecraft.client.Minecraft
+import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
/**
@@ -47,6 +55,23 @@ class SearchFilter(val pokedexManager: AbstractPokedexManager, val searchString:
}
return abilityList.any { it.contains(searchString.trim().lowercase()) }
}
+ SearchByType.MOVES -> {
+ if (pokedexManager.getHighestKnowledgeFor(entry) !== PokedexEntryProgress.CAUGHT) return false
+ val search = searchString.trim().lowercase()
+ val learnedTMs = CobblemonClient.clientTMMoveData.learnedTMs
+ val forms = pokedexManager.getCaughtForms(entry)
+ val formData = if (forms.isEmpty()) {
+ listOf(species.standardForm)
+ } else {
+ forms.map { form ->
+ species.forms.find { it.name.equals(form.displayForm, ignoreCase = true) } ?: species.standardForm
+ }
+ }
+ return formData.any { form ->
+ val highestLevel = pokedexManager.getSpeciesRecord(form.species.resourceIdentifier)?.getFormRecord(form.name)?.highestLevel ?: -1
+ hasDiscoveredMove(form, search, highestLevel, pokedexManager, learnedTMs)
+ }
+ }
SearchByType.DROPS -> {
if (pokedexManager.getHighestKnowledgeFor(entry) !== PokedexEntryProgress.CAUGHT) return false
val dropsList = mutableListOf<String>()
@@ -68,4 +93,132 @@ class SearchFilter(val pokedexManager: AbstractPokedexManager, val searchString:
}
}
-}
\ No newline at end of file
+ private fun hasDiscoveredMove(
+ form: FormData,
+ search: String,
+ highestLevel: Int,
+ pokedex: AbstractPokedexManager,
+ learnedTMs: Set<ResourceLocation>
+ ): Boolean {
+ val evolutionMoveLevels = buildEvolutionMoveLevelIndex(form)
+
+ fun matches(move: MoveTemplate): Boolean {
+ return move.displayName.string.lowercase().contains(search)
+ }
+
+ fun isLevelUpDiscovered(move: MoveTemplate, level: Int): Boolean {
+ if (highestLevel >= level) return true
+
+ for ((evolutionSpeciesId, moveLevels) in evolutionMoveLevels) {
+ val evolutionLevel = moveLevels[move.name] ?: continue
+ val evolutionHighest = pokedex.getSpeciesRecord(evolutionSpeciesId)?.highestLevel ?: -1
+ if (evolutionHighest >= evolutionLevel) return true
+ }
+ return false
+ }
+
+ form.moves.levelUpMoves.forEach { (level, moves) ->
+ for (move in moves) {
+ if (matches(move) && isLevelUpDiscovered(move, level)) return true
+ }
+ }
+
+ form.moves.tmMoves.forEach { move ->
+ val tmId = TechnicalMachines.moveToTM[move]?.id
+ val discovered = tmId == null || tmId in learnedTMs
+ if (discovered && matches(move)) return true
+ }
+
+ val alwaysDiscovered = sequenceOf(
+ form.moves.tutorMoves,
+ form.moves.eggMoves,
+ form.moves.evolutionMoves,
+ form.moves.formChangeMoves,
+ form.moves.specialMoves,
+ form.moves.legacyMoves
+ )
+
+ for (moves in alwaysDiscovered) {
+ for (move in moves) {
+ if (matches(move)) return true
+ }
+ }
+
+ return false
+ }
+
+ private fun buildEvolutionMoveLevelIndex(form: FormData): Map<ResourceLocation, Map<String, Int>> {
+ val evolutionForms = collectEvolutionForms(form)
+ val levelsBySpecies = mutableMapOf<ResourceLocation, MutableMap<String, Int>>()
+
+ for (evolutionForm in evolutionForms) {
+ val speciesId = evolutionForm.species.resourceIdentifier
+ val moveLevels = levelsBySpecies.getOrPut(speciesId) { mutableMapOf() }
+ buildMoveLevelIndex(evolutionForm).forEach { (moveName, level) ->
+ val current = moveLevels[moveName]
+ if (current == null || level < current) {
+ moveLevels[moveName] = level
+ }
+ }
+ }
+
+ return levelsBySpecies
+ }
+
+ private fun buildMoveLevelIndex(form: FormData): Map<String, Int> {
+ val levels = mutableMapOf<String, Int>()
+ form.moves.levelUpMoves.forEach { (level, moves) ->
+ moves.forEach { move ->
+ val current = levels[move.name]
+ if (current == null || level < current) {
+ levels[move.name] = level
+ }
+ }
+ }
+ return levels
+ }
+
+ private fun collectEvolutionForms(rootForm: FormData): List<FormData> {
+ val results = mutableListOf<FormData>()
+ val visited = mutableSetOf<String>()
+
+ fun key(form: FormData): String {
+ return "${form.species.resourceIdentifier}|${form.name.lowercase()}"
+ }
+
+ fun nextEvolutions(current: FormData): Set<Evolution> {
+ return if (current.evolutions.isNotEmpty()) {
+ current.evolutions
+ } else {
+ current.species.evolutions
+ }
+ }
+
+ fun traverse(current: FormData) {
+ for (evolution in nextEvolutions(current)) {
+ val evolutionForm = resolveEvolutionForm(evolution) ?: continue
+ val evolutionKey = key(evolutionForm)
+ if (!visited.add(evolutionKey)) continue
+ results.add(evolutionForm)
+ traverse(evolutionForm)
+ }
+ }
+
+ traverse(rootForm)
+ return results
+ }
+
+ private fun resolveEvolutionForm(evolution: Evolution): FormData? {
+ val speciesId = evolution.result.species?.asIdentifierDefaultingNamespace() ?: return null
+ val species = PokemonSpecies.getByIdentifier(speciesId) ?: return null
+ val formId = evolution.result.form
+ return if (formId == null) {
+ species.standardForm
+ } else {
+ species.forms.firstOrNull {
+ it.formOnlyShowdownId().equals(formId, ignoreCase = true) || it.name.equals(formId, ignoreCase = true)
+ } ?: species.standardForm
+ }
+ }
+
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SecondaryPokedexFilter.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SecondaryPokedexFilter.kt
new file mode 100644
index 0000000000..2df36b0e4a
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokedex/filter/SecondaryPokedexFilter.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.pokedex.filter
+
+import com.cobblemon.mod.common.api.moves.MoveTemplate
+import com.cobblemon.mod.common.api.pokedex.AbstractPokedexManager
+import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
+import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
+import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
+import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.client.CobblemonClient
+import com.cobblemon.mod.common.pokemon.FormData
+import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
+import net.minecraft.resources.ResourceLocation
+
+enum class SecondaryPokedexFilterType {
+ ALL,
+ CAUGHT,
+ SEEN,
+ UNDISCOVERED,
+ LEVEL_UP_TM_UNDISCOVERED
+}
+
+class SecondaryPokedexFilter(
+ private val pokedexManager: AbstractPokedexManager,
+ private val filterType: SecondaryPokedexFilterType
+) : EntryFilter() {
+
+ override fun test(entry: PokedexEntry): Boolean {
+ return when (filterType) {
+ SecondaryPokedexFilterType.ALL -> true
+ SecondaryPokedexFilterType.CAUGHT -> pokedexManager.getHighestKnowledgeFor(entry) >= PokedexEntryProgress.CAUGHT
+ SecondaryPokedexFilterType.SEEN -> pokedexManager.getHighestKnowledgeFor(entry) == PokedexEntryProgress.ENCOUNTERED
+ SecondaryPokedexFilterType.UNDISCOVERED -> pokedexManager.getHighestKnowledgeFor(entry) == PokedexEntryProgress.NONE
+ SecondaryPokedexFilterType.LEVEL_UP_TM_UNDISCOVERED -> hasUndiscoveredLevelUpTM(entry)
+ }
+ }
+
+ private fun hasUndiscoveredLevelUpTM(entry: PokedexEntry): Boolean {
+ if (pokedexManager.getHighestKnowledgeFor(entry) != PokedexEntryProgress.CAUGHT) return false
+ val species = PokemonSpecies.getByIdentifier(entry.speciesId) ?: return false
+ val forms = pokedexManager.getCaughtForms(entry)
+ val formData = if (forms.isEmpty()) {
+ listOf(species.standardForm)
+ } else {
+ forms.map { form ->
+ species.forms.find { it.name.equals(form.displayForm, ignoreCase = true) } ?: species.standardForm
+ }
+ }
+
+ return formData.any { form ->
+ val highestLevel = pokedexManager.getSpeciesRecord(form.species.resourceIdentifier)?.getFormRecord(form.name)?.highestLevel ?: -1
+ hasUndiscoveredLevelUpTM(form, highestLevel)
+ }
+ }
+
+ private fun hasUndiscoveredLevelUpTM(
+ form: FormData,
+ highestLevel: Int
+ ): Boolean {
+ val evolutionMoveLevels = buildEvolutionMoveLevelIndex(form)
+
+ fun isLevelUpDiscovered(move: MoveTemplate, level: Int): Boolean {
+ if (highestLevel >= level) return true
+
+ for ((evolutionSpeciesId, moveLevels) in evolutionMoveLevels) {
+ val evolutionLevel = moveLevels[move.name] ?: continue
+ val evolutionHighest = pokedexManager.getSpeciesRecord(evolutionSpeciesId)?.highestLevel ?: -1
+ if (evolutionHighest >= evolutionLevel) return true
+ }
+
+ return false
+ }
+
+ form.moves.levelUpMoves.forEach { (level, moves) ->
+ for (move in moves) {
+ if (TechnicalMachines.moveToTM[move] != null && !isLevelUpDiscovered(move, level)) {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+
+ private fun buildEvolutionMoveLevelIndex(form: FormData): Map<ResourceLocation, Map<String, Int>> {
+ val evolutionForms = collectEvolutionForms(form)
+ val levelsBySpecies = mutableMapOf<ResourceLocation, MutableMap<String, Int>>()
+
+ for (evolutionForm in evolutionForms) {
+ val speciesId = evolutionForm.species.resourceIdentifier
+ val moveLevels = levelsBySpecies.getOrPut(speciesId) { mutableMapOf() }
+ buildMoveLevelIndex(evolutionForm).forEach { (moveName, level) ->
+ val current = moveLevels[moveName]
+ if (current == null || level < current) {
+ moveLevels[moveName] = level
+ }
+ }
+ }
+
+ return levelsBySpecies
+ }
+
+ private fun buildMoveLevelIndex(form: FormData): Map<String, Int> {
+ val levels = mutableMapOf<String, Int>()
+ form.moves.levelUpMoves.forEach { (level, moves) ->
+ moves.forEach { move ->
+ val current = levels[move.name]
+ if (current == null || level < current) {
+ levels[move.name] = level
+ }
+ }
+ }
+ return levels
+ }
+
+ private fun collectEvolutionForms(rootForm: FormData): List<FormData> {
+ val results = mutableListOf<FormData>()
+ val visited = mutableSetOf<String>()
+
+ fun key(form: FormData): String {
+ return "${form.species.resourceIdentifier}|${form.name.lowercase()}"
+ }
+
+ fun nextEvolutions(current: FormData): Set<Evolution> {
+ return if (current.evolutions.isNotEmpty()) {
+ current.evolutions
+ } else {
+ current.species.evolutions
+ }
+ }
+
+ fun traverse(current: FormData) {
+ for (evolution in nextEvolutions(current)) {
+ val evolutionForm = resolveEvolutionForm(evolution) ?: continue
+ val evolutionKey = key(evolutionForm)
+ if (!visited.add(evolutionKey)) continue
+ results.add(evolutionForm)
+ traverse(evolutionForm)
+ }
+ }
+
+ traverse(rootForm)
+ return results
+ }
+
+ private fun resolveEvolutionForm(evolution: Evolution): FormData? {
+ val speciesId = evolution.result.species?.asIdentifierDefaultingNamespace() ?: return null
+ val species = PokemonSpecies.getByIdentifier(speciesId) ?: return null
+ val formId = evolution.result.form
+ return if (formId == null) {
+ species.standardForm
+ } else {
+ species.forms.firstOrNull {
+ it.formOnlyShowdownId().equals(formId, ignoreCase = true) || it.name.equals(formId, ignoreCase = true)
+ } ?: species.standardForm
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokemon/moves/Learnset.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokemon/moves/Learnset.kt
index bdff7f678b..870d5a2193 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/pokemon/moves/Learnset.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/pokemon/moves/Learnset.kt
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.api.pokemon.moves
+import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.data.ClientDataSynchronizer
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
@@ -19,9 +20,7 @@ import com.google.gson.JsonElement
import net.minecraft.network.RegistryFriendlyByteBuf
open class Learnset : ClientDataSynchronizer<Learnset> {
- class Interpreter(
- val loadMove: (JsonElement, Learnset) -> Boolean
- ) {
+ class Interpreter(val loadMove: (JsonElement, Learnset) -> Boolean) {
companion object {
fun parseFromPrefixIntoList(prefix: String, list: (Learnset) -> MutableList<MoveTemplate>): Interpreter {
return Interpreter { element, learnset ->
@@ -73,8 +72,7 @@ open class Learnset : ClientDataSynchronizer<Learnset> {
eggInterpreter,
tutorInterpreter,
legacyInterpreter,
- specialInterpreter,
- levelUpInterpreter,
+ specialInterpreter, levelUpInterpreter,
formChangeInterpreter
)
}
@@ -99,6 +97,20 @@ open class Learnset : ClientDataSynchronizer<Learnset> {
.flatMap { it.value }
.toSet()
+ fun tmLearnableMoves(): MutableList<MoveTemplate> {
+ val moves = mutableListOf<MoveTemplate>()
+
+ moves.addAll(tmMoves)
+ moves.addAll(tutorMoves)
+ moves.addAll(eggMoves)
+ moves.addAll(getLevelUpMovesUpTo(Cobblemon.config.maxPokemonLevel))
+ moves.addAll(evolutionMoves)
+ moves.addAll(formChangeMoves)
+
+ return moves
+ }
+
+
fun getAllLegalMoves(): Set<MoveTemplate> {
return levelUpMoves.values.flatten().toSet() +
eggMoves +
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/PokemonStore.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/PokemonStore.kt
index 5a045d28e8..8fcc964441 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/PokemonStore.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/PokemonStore.kt
@@ -77,6 +77,7 @@ abstract class PokemonStore<T : StorePosition> : Iterable<Pokemon> {
remove(pokemon)
val position = getFirstAvailablePosition() ?: return false // Couldn't fit, shrug emoji
set(position, pokemon)
+ Cobblemon.playerDataManager.getTMData(uuid)?.syncTMsFromPokemon(pokemon)
return true
}
@@ -170,4 +171,4 @@ abstract class PokemonStore<T : StorePosition> : Iterable<Pokemon> {
* each change, accessed by [Pokemon.getChangeObservable].
*/
abstract fun getAnyChangeObservable(): Observable<Unit>
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/pc/PCStore.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/pc/PCStore.kt
index d9e2141bab..124fe0cdd2 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/pc/PCStore.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/pc/PCStore.kt
@@ -332,7 +332,7 @@ open class PCStore(
val toast = Toast(
title = lang("wallpaper_unlocked"),
description = unlockableWallpaper.displayName?.let { "\"".text().add(it.asTranslated()).add("\"") } ?: lang("unknown_wallpaper") ,
- icon = ItemStack(CobblemonItems.PC)
+ icons = listOf(ItemStack(CobblemonItems.PC))
)
toast.addListeners(player)
toast.expireAfter(5F)
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/PlayerInstancedDataStoreManager.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/PlayerInstancedDataStoreManager.kt
index 11be09b9b1..d8615494de 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/PlayerInstancedDataStoreManager.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/PlayerInstancedDataStoreManager.kt
@@ -8,14 +8,17 @@
package com.cobblemon.mod.common.api.storage.player
+import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.MODID
import com.cobblemon.mod.common.api.pokedex.PokedexManager
import com.cobblemon.mod.common.api.scheduling.ScheduledTask
import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
+import com.cobblemon.mod.common.api.storage.player.client.ClientInstancedPlayerData
+import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
+import com.cobblemon.mod.common.api.tms.TMMoveManager
import com.google.common.util.concurrent.ThreadFactoryBuilder
import java.util.UUID
import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Player
@@ -30,7 +33,6 @@ import net.minecraft.world.entity.player.Player
* @author Apion
* @since February 21, 2024
*/
-
open class PlayerInstancedDataStoreManager {
val saveExecutor = Executors.newSingleThreadExecutor(
ThreadFactoryBuilder()
@@ -53,19 +55,27 @@ open class PlayerInstancedDataStoreManager {
//Should put this somewhere else
saveTasks[PlayerInstancedDataStoreTypes.GENERAL] = ScheduledTask.Builder()
.execute { saveAllOfOneType(PlayerInstancedDataStoreTypes.GENERAL) }
- .delay(30f)
- .interval(120f)
+ .delay(30F)
+ .interval(120F)
.infiniteIterations()
.tracker(ServerTaskTracker)
.build()
saveTasks[PlayerInstancedDataStoreTypes.POKEDEX] = ScheduledTask.Builder()
.execute { saveAllOfOneType(PlayerInstancedDataStoreTypes.POKEDEX) }
- .delay(30f)
- .interval(120f)
+ .delay(30F)
+ .interval(120F)
.infiniteIterations()
.tracker(ServerTaskTracker)
.build()
+
+ saveTasks[PlayerInstancedDataStoreTypes.TM_MOVES] = ScheduledTask.Builder()
+ .execute { saveAllOfOneType(PlayerInstancedDataStoreTypes.TM_MOVES) }
+ .delay(30F)
+ .interval(120F)
+ .infiniteIterations()
+ .tracker(ServerTaskTracker)
+ .build()
}
open fun get(playerId: UUID, dataType: PlayerInstancedDataStoreType): InstancedPlayerData {
@@ -126,4 +136,16 @@ open class PlayerInstancedDataStoreManager {
open fun getPokedexData(playerId: UUID): PokedexManager {
return get(playerId, PlayerInstancedDataStoreTypes.POKEDEX) as PokedexManager
}
-}
\ No newline at end of file
+
+ fun getTMData(player: ServerPlayer): TMMoveManager {
+ return getTMData(player.uuid)
+ }
+
+ fun getTMData(playerId: UUID): TMMoveManager {
+ val data = get(playerId, PlayerInstancedDataStoreTypes.TM_MOVES)
+ if (data !is TMMoveManager) {
+ Cobblemon.LOGGER.warn("getTMData: No TMMoveManager found for $playerId (got ${data?.javaClass?.name})")
+ }
+ return data as TMMoveManager
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/PlayerInstancedDataStoreTypes.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/PlayerInstancedDataStoreTypes.kt
index 2243c69f9b..6b19992c4f 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/PlayerInstancedDataStoreTypes.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/PlayerInstancedDataStoreTypes.kt
@@ -8,8 +8,10 @@
package com.cobblemon.mod.common.api.storage.player
-import com.cobblemon.mod.common.api.storage.player.client.ClientGeneralPlayerData
+import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
+import com.cobblemon.mod.common.api.storage.player.client.ClientGeneralPlayerData
+import com.cobblemon.mod.common.api.storage.player.client.ClientTMMoveManager
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
@@ -27,11 +29,22 @@ object PlayerInstancedDataStoreTypes {
ClientPokedexManager::runAction,
ClientPokedexManager::runIncremental
))
+ val TM_MOVES = register(PlayerInstancedDataStoreType(
+ cobblemonResource("tm_moves"),
+ ClientTMMoveManager::decode,
+ ClientTMMoveManager::runAction,
+ ClientTMMoveManager::runIncremental
+ ))
fun register(type: PlayerInstancedDataStoreType): PlayerInstancedDataStoreType {
+ Cobblemon.LOGGER.debug("Registering PlayerInstancedDataStoreType: {}", type.id)
types[type.id] = type
return type
}
- fun getTypeById(id: ResourceLocation) = types[id]
-}
\ No newline at end of file
+ fun getTypeById(id: ResourceLocation): PlayerInstancedDataStoreType? {
+ val type = types[id]
+ if (type == null) Cobblemon.LOGGER.warn("Unknown PlayerInstancedDataStoreType ID: {}", id)
+ return type
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/adapter/TMMoveJsonBackend.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/adapter/TMMoveJsonBackend.kt
new file mode 100644
index 0000000000..89a8f06f57
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/adapter/TMMoveJsonBackend.kt
@@ -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.storage.player.adapter
+
+import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
+import com.cobblemon.mod.common.api.tms.TMMoveManager
+import com.cobblemon.mod.common.util.adapters.CodecBackedAdapter
+import com.google.gson.GsonBuilder
+import com.google.gson.reflect.TypeToken
+import java.util.UUID
+
+class TMMoveJsonBackend : JsonBackedPlayerDataStoreBackend<TMMoveManager>("tm_moves", PlayerInstancedDataStoreTypes.TM_MOVES) {
+ override val gson = GsonBuilder()
+ .setPrettyPrinting()
+ .disableHtmlEscaping()
+ .registerTypeAdapter(TMMoveManager::class.java, CodecBackedAdapter(TMMoveManager.CODEC))
+ .create()
+
+ override val classToken = TypeToken.get(TMMoveManager::class.java)
+ override val defaultData = defaultDataFunc
+
+ override fun initialize(store: TMMoveManager) {
+ store.initialize()
+ }
+
+ companion object {
+ val defaultDataFunc = { uuid: UUID -> TMMoveManager(uuid) }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/adapter/TMMoveMongoBackend.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/adapter/TMMoveMongoBackend.kt
new file mode 100644
index 0000000000..e4b7fda155
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/adapter/TMMoveMongoBackend.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.storage.player.adapter
+
+import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
+import com.cobblemon.mod.common.api.tms.TMMoveManager
+import com.cobblemon.mod.common.util.adapters.CodecBackedAdapter
+import com.google.gson.GsonBuilder
+import com.google.gson.reflect.TypeToken
+import com.mongodb.client.MongoClient
+import java.util.UUID
+
+class TMMoveMongoBackend(
+ mongoClient: MongoClient,
+ databaseName: String,
+ collectionName: String
+) : MongoBackedPlayerDataStoreBackend<TMMoveManager>(
+ mongoClient,
+ databaseName,
+ collectionName,
+ PlayerInstancedDataStoreTypes.TM_MOVES
+) {
+ override val gson = GsonBuilder()
+ .registerTypeAdapter(TMMoveManager::class.java, CodecBackedAdapter(TMMoveManager.CODEC))
+ .create()
+ override val classToken = TypeToken.get(TMMoveManager::class.java)
+ override val defaultData = defaultDataFunc
+
+ override fun initialize(store: TMMoveManager) {
+ store.initialize()
+ }
+
+ companion object {
+ val defaultDataFunc = { uuid: UUID -> TMMoveManager(uuid) }
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/adapter/TMMoveNbtBackend.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/adapter/TMMoveNbtBackend.kt
new file mode 100644
index 0000000000..544ec919b2
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/adapter/TMMoveNbtBackend.kt
@@ -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.storage.player.adapter
+
+import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
+import com.cobblemon.mod.common.api.tms.TMMoveManager
+import java.util.UUID
+
+class TMMoveNbtBackend : NbtBackedPlayerData<TMMoveManager>("tm_moves", PlayerInstancedDataStoreTypes.TM_MOVES) {
+ override val codec = TMMoveManager.CODEC
+ override val defaultData = TMMoveJsonBackend.defaultDataFunc
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/client/ClientTMMoveManager.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/client/ClientTMMoveManager.kt
new file mode 100644
index 0000000000..54e500b45d
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/client/ClientTMMoveManager.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.storage.player.client
+
+import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
+import com.cobblemon.mod.common.client.CobblemonClient
+import com.cobblemon.mod.common.net.messages.client.SetClientPlayerDataPacket
+import com.cobblemon.mod.common.util.readString
+import com.cobblemon.mod.common.util.writeString
+import net.minecraft.network.RegistryFriendlyByteBuf
+import net.minecraft.resources.ResourceLocation
+
+class ClientTMMoveManager(
+ val learnedTMs: MutableSet<ResourceLocation>
+) : ClientInstancedPlayerData {
+
+ override fun encode(buf: RegistryFriendlyByteBuf) {
+ buf.writeCollection(learnedTMs) { _, id -> buf.writeString(id.toString()) }
+ }
+
+ companion object {
+ fun decode(buf: RegistryFriendlyByteBuf): SetClientPlayerDataPacket {
+ val tms = buf.readCollection(
+ { mutableSetOf<ResourceLocation>() },
+ { buf.readString().let(ResourceLocation::tryParse) ?: ResourceLocation("minecraft", "empty") }
+ )
+ return SetClientPlayerDataPacket(
+ PlayerInstancedDataStoreTypes.TM_MOVES,
+ ClientTMMoveManager(tms)
+ )
+ }
+
+ fun runAction(data: ClientInstancedPlayerData) {
+ if (data !is ClientTMMoveManager) return
+ CobblemonClient.clientTMMoveData = data
+ }
+
+ fun runIncremental(data: ClientInstancedPlayerData) {
+ if (data !is ClientTMMoveManager) return
+ CobblemonClient.clientTMMoveData.learnedTMs.addAll(data.learnedTMs)
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/factory/CachedPlayerDataStoreFactory.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/factory/CachedPlayerDataStoreFactory.kt
index 24bea63adb..b31ab888f8 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/factory/CachedPlayerDataStoreFactory.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/storage/player/factory/CachedPlayerDataStoreFactory.kt
@@ -40,8 +40,6 @@ class CachedPlayerDataStoreFactory<T : InstancedPlayerData>(val backend: PlayerD
}
}
-
-
override fun saveAll() {
cache.forEach { (_, pd) -> backend.save(pd) }
cache.removeIf { (uuid, _) -> uuid.getPlayer() == null }
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/tags/CobblemonBlockTags.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/tags/CobblemonBlockTags.kt
index 6079c4bf16..9afa850afe 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/tags/CobblemonBlockTags.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/tags/CobblemonBlockTags.kt
@@ -91,6 +91,7 @@ object CobblemonBlockTags {
@JvmField val TUMBLESTONE_BRICKS = createTag("tumblestone_bricks")
@JvmField val TUMBLESTONES = createTag("tumblestones")
@JvmField val TUMBLESTONE_HEAT_SOURCE = createTag("tumblestone_heat_source")
+ @JvmField val TYPE_GEM_BLOCKS = createTag("type_gem_blocks")
@JvmField val WALL_HANGING_SIGNS = createTag("wall_hanging_signs")
@JvmField val WALL_SIGNS = createTag("wall_signs")
@JvmField val WATER_STONE_ORES = createTag("water_stone_ores")
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/ObtainMethod.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/ObtainMethod.kt
new file mode 100644
index 0000000000..3a645060c3
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/ObtainMethod.kt
@@ -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.tms
+
+import com.cobblemon.mod.common.tms.obtain.DefaultObtainMethod
+import com.cobblemon.mod.common.tms.obtain.PlayerHasAdvancementObtainMethod
+import com.cobblemon.mod.common.tms.obtain.PlayerYObtainMethod
+import com.cobblemon.mod.common.tms.obtain.UnlockableObtainMethod
+import net.minecraft.network.RegistryFriendlyByteBuf
+import net.minecraft.server.level.ServerPlayer
+
+interface ObtainMethod {
+ val passive: Boolean
+
+ /**
+ * Checks if this [ObtainMethod] matches the given player.
+ */
+ fun matches(player: ServerPlayer): Boolean
+
+ /**
+ * Serializes this [ObtainMethod] to the buffer.
+ */
+ fun writeToBuffer(buffer: RegistryFriendlyByteBuf)
+
+ companion object {
+ /**
+ * Deserializes an [ObtainMethod] from the buffer.
+ */
+ fun readFromBuffer(buffer: RegistryFriendlyByteBuf): ObtainMethod {
+ val variant = buffer.readUtf()
+ return when (variant) {
+ "cobblemon:unlockable" -> UnlockableObtainMethod()
+ "cobblemon:default" -> DefaultObtainMethod()
+ "cobblemon:advancement" -> {
+ val advancement = buffer.readResourceLocation()
+ PlayerHasAdvancementObtainMethod(advancement)
+ }
+ "cobblemon:y_level" -> {
+ val yLevel = buffer.readVarInt()
+ val operator = buffer.readUtf()
+ PlayerYObtainMethod(yLevel, operator)
+ }
+ else -> throw IllegalArgumentException("Unknown ObtainMethod variant: $variant")
+ }
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TMMoveManager.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TMMoveManager.kt
new file mode 100644
index 0000000000..a6e250d2b9
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TMMoveManager.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.tms
+
+import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
+import com.cobblemon.mod.common.api.moves.MoveTemplate
+import com.cobblemon.mod.common.api.scheduling.ScheduledTask
+import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
+import com.cobblemon.mod.common.api.storage.player.InstancedPlayerData
+import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
+import com.cobblemon.mod.common.api.storage.player.client.ClientTMMoveManager
+import com.cobblemon.mod.common.net.messages.client.SetClientPlayerDataPacket
+import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
+import com.cobblemon.mod.common.pokemon.Pokemon
+import com.cobblemon.mod.common.util.getPlayer
+import com.cobblemon.mod.common.util.lang
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import java.util.UUID
+import net.minecraft.network.chat.Component
+import net.minecraft.resources.ResourceLocation
+
+class TMMoveManager(
+ override val uuid: UUID,
+ val learnedTMs: MutableSet<ResourceLocation> = mutableSetOf()
+) : InstancedPlayerData {
+
+ val storeType = PlayerInstancedDataStoreTypes.TM_MOVES
+
+ override fun toClientData() = ClientTMMoveManager(learnedTMs.toMutableSet())
+ fun toClientDataFrom(set: Set<ResourceLocation>) = ClientTMMoveManager(set.toMutableSet())
+
+ fun syncTMsFromPokemon(pokemon: Pokemon) {
+ learn(getLearnableTMsFromPokemon(pokemon))
+ }
+
+ fun syncTMFromMove(moveTemplate: MoveTemplate) {
+ val tmId = TechnicalMachines.moveToTM[moveTemplate]?.id ?: return
+ learn(listOf(tmId))
+ }
+
+ fun getLearnableTMsFromPokemon(pokemon: Pokemon): Collection<ResourceLocation> {
+ return TechnicalMachines.tmMap.values
+ .filter { tm -> tm.moveName in pokemon.allAccessibleMoves }
+ .map(TechnicalMachine::id)
+ }
+
+ fun scheduleFullSyncFromStores(
+ party: Iterable<Pokemon?>,
+ pc: Iterable<Pokemon?>,
+ batchSize: Int = 25
+ ) {
+ val iterator = sequence {
+ for (pokemon in party) {
+ if (pokemon != null) yield(pokemon)
+ }
+ for (pokemon in pc) {
+ if (pokemon != null) yield(pokemon)
+ }
+ }.iterator()
+
+ if (!iterator.hasNext()) return
+
+ val tmIds = mutableSetOf<ResourceLocation>()
+
+ ScheduledTask.Builder()
+ .tracker(ServerTaskTracker)
+ .interval(0F)
+ .infiniteIterations()
+ .execute { task ->
+ var processed = 0
+ while (processed < batchSize && iterator.hasNext()) {
+ val pokemon = iterator.next()
+ for (move in pokemon.allAccessibleMoves) {
+ TechnicalMachines.moveToTM[move]?.id?.let { tmIds.add(it) }
+ }
+ processed++
+ }
+
+ if (!iterator.hasNext()) {
+ learn(tmIds)
+ task.expire()
+ }
+ }
+ .build()
+ }
+
+ fun learn(tmIds: Collection<ResourceLocation>): Boolean {
+ val newLearnedTms = mutableListOf<ResourceLocation>()
+ for (tmId in tmIds) {
+ if (learnedTMs.add(tmId)) {
+ newLearnedTms.add(tmId)
+ }
+ }
+
+ if (newLearnedTms.isEmpty()) return false
+
+ syncClient(newLearnedTms.toSet())
+ sendTMToast(newLearnedTms)
+
+ return true
+ }
+
+ fun unlearn(tmIds: Collection<ResourceLocation>): Boolean {
+ val removedTms = mutableListOf<ResourceLocation>()
+
+ for (tmId in tmIds) {
+ if (learnedTMs.remove(tmId)) {
+ removedTms.add(tmId)
+ }
+ }
+
+ if (removedTms.isEmpty()) return false
+
+ syncClient(learnedTMs, isIncremental = false)
+
+ return true
+ }
+
+ fun sendTMToast(tmIds: List<ResourceLocation>) {
+ val player = uuid.getPlayer() ?: return
+ var moveName: Component = Component.empty()
+ val icons = tmIds.mapNotNull {
+ val tm = TechnicalMachines.tmMap[it] ?: return@mapNotNull null
+ moveName = tm.translatedMoveName()
+
+ return@mapNotNull tm.createItemStack()
+ }
+
+ val description = if (icons.size == 1) moveName else lang("tms.check_tmm")
+
+ val packet = ToastPacket(
+ title = lang("tms.new_tms_learned"),
+ description = description,
+ icons = icons,
+ frameTexture = ResourceLocation.parse("minecraft:toast/advancement"),
+ progress = -1F,
+ progressColor = 0x00FF00,
+ // Keeping it a fixed UUID makes it so the ToastTracker merges different toasts on the client
+ uuid = UUID.nameUUIDFromBytes("tm_toast".toByteArray()),
+ behaviour = ToastPacket.Behaviour.SHOW_OR_UPDATE,
+ durationMs = 4000L
+ )
+
+ player.sendPacket(packet)
+ }
+
+ fun syncClient(updateSet: Set<ResourceLocation> = learnedTMs, isIncremental: Boolean = true) {
+ uuid.getPlayer()?.sendPacket(
+ SetClientPlayerDataPacket(
+ type = storeType,
+ playerData = toClientDataFrom(updateSet),
+ isIncremental = isIncremental
+ )
+ )
+ }
+
+ companion object {
+ val CODEC: Codec<TMMoveManager> = RecordCodecBuilder.create { instance ->
+ instance.group(
+ Codec.STRING.xmap(UUID::fromString, UUID::toString)
+ .fieldOf("uuid")
+ .forGetter { it.uuid },
+ Codec.list(ResourceLocation.CODEC)
+ .xmap({ it.toMutableSet() }, { it.toList() })
+ .fieldOf("learnedTMs")
+ .forGetter { it.learnedTMs }
+ ).apply(instance, ::TMMoveManager)
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachine.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachine.kt
new file mode 100644
index 0000000000..d462372102
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachine.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.tms
+
+import com.cobblemon.mod.common.CobblemonItems
+import com.cobblemon.mod.common.api.moves.MoveTemplate
+import com.cobblemon.mod.common.api.types.ElementalType
+import com.cobblemon.mod.common.api.types.ElementalTypes
+import com.cobblemon.mod.common.client.CobblemonClient
+import com.cobblemon.mod.common.item.components.TMMoveComponent
+import com.cobblemon.mod.common.pokemon.Pokemon
+import com.cobblemon.mod.common.tms.obtain.DefaultObtainMethod
+import com.cobblemon.mod.common.util.lang
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.server.level.ServerPlayer
+import net.minecraft.network.chat.MutableComponent
+import net.minecraft.world.entity.player.Player
+import net.minecraft.world.item.ItemStack
+
+class TechnicalMachine(
+ val moveName: MoveTemplate,
+ val recipe: List<TechnicalMachineRecipe>?,
+ val obtainMethods: List<ObtainMethod> = emptyList(),
+ val type: String
+) {
+ companion object {
+ /**
+ * Filters all available [TechnicalMachines] based on three optional arguments
+ *
+ * @param search A [String] to check against move names
+ * @param type An [ElementalType] to check for
+ * @param pokemon A [Pokemon] to check the [Learnset] of
+ * @return A [MutableSet] of [TechnicalMachine] that passed the filter
+ */
+ fun filterTms(
+ search: String?,
+ type: ElementalType?,
+ pokemon: Pokemon?,
+ player: Player? = null,
+ includeUnlearned: Boolean = false
+ ): MutableSet<TechnicalMachine> {
+ val tms = TechnicalMachines.tmMap.values.toMutableSet()
+
+ type?.let {
+ val iterator = tms.iterator()
+ while (iterator.hasNext()) {
+ val tm = iterator.next()
+ if (ElementalTypes.get(tm.type) != type) {
+ iterator.remove()
+ }
+ }
+ }
+
+ pokemon?.let {
+ val iterator = tms.iterator()
+ while (iterator.hasNext()) {
+ val tm = iterator.next()
+ if (tm.moveName !in pokemon.form.moves.tmLearnableMoves()) {
+ iterator.remove()
+ }
+ }
+ }
+
+ search?.let {
+ val iterator = tms.iterator()
+ while (iterator.hasNext()) {
+ val tm = iterator.next()
+ if (!tm.translatedMoveName().string.contains(search, ignoreCase = true)) {
+ iterator.remove()
+ }
+ }
+ }
+
+ if (player != null && !includeUnlearned) {
+ val learnedTMs = CobblemonClient.clientTMMoveData.learnedTMs
+ tms.retainAll { tm -> tm.isPassivelyObtained() || tm.id in learnedTMs }
+ }
+
+ return tms
+ }
+ }
+
+ lateinit var id: ResourceLocation
+
+ /**
+ * Gets the recipe list limited to the provided amount to match the slot amount of the TM Machine.
+ *
+ * @param limit The amount of recipe item slots to take.
+ * @return The recipe list limited to 3 ingredients.
+ */
+ fun getClampedRecipe(limit: Int = 3): List<TechnicalMachineRecipe>? = recipe?.take(limit)
+
+ /**
+ * Unlocks this [TechnicalMachine] for the player.
+ *
+ * @param player The [ServerPlayer] to give this [TechnicalMachine] to.
+ * @return Whether the player was successfully granted the [TechnicalMachine]
+ */
+ fun unlock(player: ServerPlayer): Boolean {
+ if (!obtainMethods.any { it is DefaultObtainMethod }) {
+ player.sendSystemMessage(lang("tms.unlock_tm", moveName.displayName))
+ }
+ return true
+ }
+
+ fun isPassivelyObtained() = obtainMethods.any { it is DefaultObtainMethod }
+
+ /**
+ * Returns a [MutableComponent] of the translated move name of this [TechnicalMachine]
+ *
+ * @return This [TechnicalMachine]'s move name, translated
+ */
+ fun translatedMoveName(): MutableComponent = moveName.displayName
+
+ fun createItemStack(): ItemStack {
+ val stack = ItemStack(CobblemonItems.TECHNICAL_MACHINE)
+ TMMoveComponent.setTMMove(stack, moveName)
+ return stack
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachineDTO.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachineDTO.kt
new file mode 100644
index 0000000000..e8755c1f9c
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachineDTO.kt
@@ -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.tms
+
+import com.cobblemon.mod.common.api.moves.MoveTemplate
+
+/**
+ * DTO used for [TechnicalMachine] JSON loading because Gson cannot deserialize Minecraft Ingredients.
+ */
+data class TechnicalMachineDTO(
+ val moveName: MoveTemplate,
+ val recipe: List<TechnicalMachineRecipeDTO>?,
+ val obtainMethods: List<ObtainMethod> = emptyList(),
+ val type: String
+) {
+ fun toTechnicalMachine(): TechnicalMachine {
+ return TechnicalMachine(
+ moveName = moveName,
+ recipe = recipe?.map { it.toTechnicalMachineRecipe() },
+ obtainMethods = obtainMethods,
+ type = type
+ )
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachineRecipe.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachineRecipe.kt
new file mode 100644
index 0000000000..0c56977edd
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachineRecipe.kt
@@ -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.tms
+
+import net.minecraft.world.item.crafting.Ingredient
+
+/**
+ * Represents the ingredients necessary to craft a [TechnicalMachine] in the [TMBlock]
+ */
+data class TechnicalMachineRecipe(
+ val ingredient: Ingredient,
+ val count: Int
+)
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachineRecipeDTO.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachineRecipeDTO.kt
new file mode 100644
index 0000000000..c78c62f299
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachineRecipeDTO.kt
@@ -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.tms
+
+import com.google.gson.annotations.SerializedName
+import net.minecraft.core.registries.BuiltInRegistries
+import net.minecraft.core.registries.Registries
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.tags.TagKey
+import net.minecraft.world.item.crafting.Ingredient
+
+/**
+ * DTO used for [TechnicalMachineRecipe] JSON loading because Gson cannot deserialize Minecraft Ingredients.
+ */
+data class TechnicalMachineRecipeDTO(
+ val item: ResourceLocation?,
+ val tag: ResourceLocation?,
+ @SerializedName("count") private val _count: Int?,
+) {
+ private val count: Int
+ get() = _count ?: 1
+
+ fun toTechnicalMachineRecipe(): TechnicalMachineRecipe {
+ val ingredient = when {
+ item != null ->
+ Ingredient.of(
+ BuiltInRegistries.ITEM.get(item)
+ )
+
+ tag != null ->
+ Ingredient.of(
+ TagKey.create(Registries.ITEM, tag)
+ )
+
+ else ->
+ error("Recipe entry must define either 'item' or 'tag'")
+ }
+
+ return TechnicalMachineRecipe(
+ ingredient = ingredient,
+ count = count
+ )
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachines.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachines.kt
new file mode 100644
index 0000000000..40f5648462
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/tms/TechnicalMachines.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.tms
+
+import com.cobblemon.mod.common.api.data.JsonDataRegistry
+import com.cobblemon.mod.common.api.moves.MoveTemplate
+import com.cobblemon.mod.common.api.moves.adapters.MoveTemplateAdapter
+import com.cobblemon.mod.common.api.reactive.SimpleObservable
+import com.cobblemon.mod.common.util.adapters.TMObtainMethodAdapter
+import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
+import com.cobblemon.mod.common.util.cobblemonResource
+import com.google.gson.GsonBuilder
+import com.google.gson.reflect.TypeToken
+import java.io.BufferedReader
+import java.util.concurrent.ExecutionException
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.server.level.ServerPlayer
+import net.minecraft.server.packs.PackType
+
+object TechnicalMachines : JsonDataRegistry<TechnicalMachine> {
+ override val gson = GsonBuilder()
+ .registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
+ .registerTypeAdapter(ObtainMethod::class.java, TMObtainMethodAdapter)
+ .registerTypeAdapter(MoveTemplate::class.java, MoveTemplateAdapter)
+ .create()
+
+ override val typeToken = TypeToken.get(TechnicalMachine::class.java)
+ override val resourcePath = "tms"
+ override val id = cobblemonResource("technical_machines")
+ override val type = PackType.SERVER_DATA
+ override val observable = SimpleObservable<TechnicalMachines>()
+
+ val tmMap = mutableMapOf<ResourceLocation, TechnicalMachine>()
+ val moveToTM = mutableMapOf<MoveTemplate, TechnicalMachine>()
+
+ /**
+ * Overridden to deserialize a DTO first and convert it to a runtime
+ * [TechnicalMachine], since Gson cannot deserialize Minecraft Ingredients
+ * directly.
+ */
+ override fun parse(
+ reader: BufferedReader,
+ identifier: ResourceLocation
+ ): TechnicalMachine {
+ try {
+ val technicalMachineDto = gson.fromJson(reader, TechnicalMachineDTO::class.java)
+ val technicalMachine = technicalMachineDto.toTechnicalMachine()
+ return technicalMachine
+ } catch (exception: Exception) {
+ throw ExecutionException(
+ "Error loading Technical Machine JSON: $identifier",
+ exception
+ )
+ }
+ }
+
+ override fun reload(data: Map<ResourceLocation, TechnicalMachine>) {
+ data.forEach { (id, tm) ->
+ tmMap[id] = tm
+ tm.id = id
+ moveToTM[tm.moveName] = tm
+ }
+ }
+
+ fun getByResourceLocation(resourceLocation: ResourceLocation) = tmMap[resourceLocation]
+ fun getAllResourceLocations() = tmMap.keys.toSet()
+ override fun sync(player: ServerPlayer) {}
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/toast/Toast.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/toast/Toast.kt
index 1fe6a25569..e9db1f320d 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/toast/Toast.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/toast/Toast.kt
@@ -28,7 +28,7 @@ import net.minecraft.world.item.ItemStack
*
* @param title The [Component] representing the toast title, this can be a [Component.empty].
* @param description The [Component] representing the toast description, this can be a [Component.empty].
- * @param icon The [ItemStack] that is rendered as the toast icon, this can be a [ItemStack.EMPTY].
+ * @param icons The [ItemStack] that is rendered as the toast icon, this can be a [ItemStack.EMPTY].
* @param frameTexture The texture of the frame, default is the [VANILLA_BACKGROUND_SPRITE].
* @param progress The value of the progress bar, this accepts a value between 0.0F and 1.0F, any other value will not render a progress bar.
* @param progressColor The progress bar color in RGB, default is the [VANILLA_PROGRESS_COLOR].
@@ -38,7 +38,7 @@ import net.minecraft.world.item.ItemStack
class Toast(
title: Component,
description: Component,
- icon: ItemStack,
+ icons: List<ItemStack>,
frameTexture: ResourceLocation = VANILLA_BACKGROUND_SPRITE,
progress: Float = -1F,
progressColor: Int = VANILLA_PROGRESS_COLOR
@@ -57,7 +57,7 @@ class Toast(
/**
* The [ItemStack] that is rendered as the toast icon, this can be a [ItemStack.EMPTY].
*/
- var icon: ItemStack by Delegates.observable(icon) { _, old, new -> if (old != new) this.launchUpdate() }
+ var icons: List<ItemStack> by Delegates.observable(icons) { _, old, new -> if (old != new) this.launchUpdate() }
/**
* The texture of the frame.
@@ -185,7 +185,7 @@ class Toast(
private fun toPacket(behaviour: ToastPacket.Behaviour): ToastPacket = ToastPacket(
this.title,
this.description,
- this.icon,
+ this.icons,
this.frameTexture,
this.progress,
this.progressColor,
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/types/ElementalType.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/types/ElementalType.kt
index 7f96d353a9..7e1d5a9b08 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/types/ElementalType.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/types/ElementalType.kt
@@ -29,9 +29,12 @@ class ElementalType(
val displayName: MutableComponent,
val hue: Int,
val textureXMultiplier: Int,
+ val primaryColor: Int,
+ val secondaryColor: Int,
val resourceLocation: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, "ui/types.png"),
- val showdownId: String = ShowdownIdentifiable.REGEX.replace(name.lowercase(), "")
-) : ShowdownIdentifiable {
+ val showdownId: String = ShowdownIdentifiable.REGEX.replace(name.lowercase(), ""),
+ val typeGem: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, showdownId + "_gem"),
+ ) : ShowdownIdentifiable {
override fun showdownId(): String {
return showdownId
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/api/types/ElementalTypes.kt b/common/src/main/kotlin/com/cobblemon/mod/common/api/types/ElementalTypes.kt
index d64363c98a..cf55e218f5 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/api/types/ElementalTypes.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/api/types/ElementalTypes.kt
@@ -23,7 +23,9 @@ object ElementalTypes {
name = "Normal",
displayName = Component.translatable("cobblemon.type.normal"),
hue = 0xE8E8DA,
- textureXMultiplier = 0
+ textureXMultiplier = 0,
+ primaryColor = 0x8FA689,
+ secondaryColor = 0xFFFBE2
)
@JvmField
@@ -31,7 +33,9 @@ object ElementalTypes {
name = "Fire",
displayName = Component.translatable("cobblemon.type.fire"),
hue = 0xFF6E21,
- textureXMultiplier = 1
+ textureXMultiplier = 1,
+ primaryColor = 0xF92D2D,
+ secondaryColor = 0xF99B1D
)
@JvmField
@@ -39,7 +43,9 @@ object ElementalTypes {
name = "Water",
displayName = Component.translatable("cobblemon.type.water"),
hue = 0x3FA5FF,
- textureXMultiplier = 2
+ textureXMultiplier = 2,
+ primaryColor = 0x377AF6,
+ secondaryColor = 0x50DDF9
)
@JvmField
@@ -47,7 +53,9 @@ object ElementalTypes {
name = "Grass",
displayName = Component.translatable("cobblemon.type.grass"),
hue = 0x62D14F,
- textureXMultiplier = 3
+ textureXMultiplier = 3,
+ primaryColor = 0x0EAE95,
+ secondaryColor = 0xC6F554
)
@JvmField
@@ -55,7 +63,9 @@ object ElementalTypes {
name = "Electric",
displayName = Component.translatable("cobblemon.type.electric"),
hue = 0xFFD314,
- textureXMultiplier = 4
+ textureXMultiplier = 4,
+ primaryColor = 0xF7AB19,
+ secondaryColor = 0xFFF257
)
@JvmField
@@ -63,7 +73,9 @@ object ElementalTypes {
name = "Ice",
displayName = Component.translatable("cobblemon.type.ice"),
hue = 0x54F2F2,
- textureXMultiplier = 5
+ textureXMultiplier = 5,
+ primaryColor = 0x41BAED,
+ secondaryColor = 0x91F4F9
)
@JvmField
@@ -71,7 +83,9 @@ object ElementalTypes {
name = "Fighting",
displayName = Component.translatable("cobblemon.type.fighting"),
hue = 0xEF565D,
- textureXMultiplier = 6
+ textureXMultiplier = 6,
+ primaryColor = 0xC8356B,
+ secondaryColor = 0xF36A3F
)
@JvmField
@@ -79,7 +93,9 @@ object ElementalTypes {
name = "Poison",
displayName = Component.translatable("cobblemon.type.poison"),
hue = 0xD651FF,
- textureXMultiplier = 7
+ textureXMultiplier = 7,
+ primaryColor = 0x7B41DD,
+ secondaryColor = 0xE568F4
)
@JvmField
@@ -87,7 +103,9 @@ object ElementalTypes {
name = "Ground",
displayName = Component.translatable("cobblemon.type.ground"),
hue = 0xF4A453,
- textureXMultiplier = 8
+ textureXMultiplier = 8,
+ primaryColor = 0xE08835,
+ secondaryColor = 0xFFD057
)
@JvmField
@@ -95,7 +113,9 @@ object ElementalTypes {
name = "Flying",
displayName = Component.translatable("cobblemon.type.flying"),
hue = 0xB8B2FF,
- textureXMultiplier = 9
+ textureXMultiplier = 9,
+ primaryColor = 0x6083F6,
+ secondaryColor = 0xD5CCFA
)
@JvmField
@@ -103,7 +123,9 @@ object ElementalTypes {
name = "Psychic",
displayName = Component.translatable("cobblemon.type.psychic"),
hue = 0xFF5E9E,
- textureXMultiplier = 10
+ textureXMultiplier = 10,
+ primaryColor = 0xE553CE,
+ secondaryColor = 0xF69DD5
)
@JvmField
@@ -111,7 +133,9 @@ object ElementalTypes {
name = "Bug",
displayName = Component.translatable("cobblemon.type.bug"),
hue = 0xD3D319,
- textureXMultiplier = 11
+ textureXMultiplier = 11,
+ primaryColor = 0x9FBE1B,
+ secondaryColor = 0xE9FB5B
)
@JvmField
@@ -119,7 +143,9 @@ object ElementalTypes {
name = "Rock",
displayName = Component.translatable("cobblemon.type.rock"),
hue = 0xB7A16E,
- textureXMultiplier = 12
+ textureXMultiplier = 12,
+ primaryColor = 0x9F814A,
+ secondaryColor = 0xEECB7A
)
@JvmField
@@ -127,7 +153,9 @@ object ElementalTypes {
name = "Ghost",
displayName = Component.translatable("cobblemon.type.ghost"),
hue = 0x9C80F7,
- textureXMultiplier = 13
+ textureXMultiplier = 13,
+ primaryColor = 0x4F57DD,
+ secondaryColor = 0xC182F1
)
@JvmField
@@ -135,7 +163,9 @@ object ElementalTypes {
name = "Dragon",
displayName = Component.translatable("cobblemon.type.dragon"),
hue = 0x7580FF,
- textureXMultiplier = 14
+ textureXMultiplier = 14,
+ primaryColor = 0x4E68E2,
+ secondaryColor = 0x47C7F7
)
@JvmField
@@ -143,7 +173,9 @@ object ElementalTypes {
name = "Dark",
displayName = Component.translatable("cobblemon.type.dark"),
hue = 0x587DA0,
- textureXMultiplier = 15
+ textureXMultiplier = 15,
+ primaryColor = 0x465B99,
+ secondaryColor = 0x689EBF
)
@JvmField
@@ -151,7 +183,9 @@ object ElementalTypes {
name = "Steel",
displayName = Component.translatable("cobblemon.type.steel"),
hue = 0xABD1F4,
- textureXMultiplier = 16
+ textureXMultiplier = 16,
+ primaryColor = 0x628CB9,
+ secondaryColor = 0xD6F7F3
)
@JvmField
@@ -159,16 +193,20 @@ object ElementalTypes {
name = "Fairy",
displayName = Component.translatable("cobblemon.type.fairy"),
hue = 0xFF7FE5,
- textureXMultiplier = 17
+ textureXMultiplier = 17,
+ primaryColor = 0xE2678C,
+ secondaryColor = 0xF5A7B7
)
@JvmStatic
- fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int): ElementalType {
+ fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int, primaryColor: Int, secondaryColor: Int): ElementalType {
return ElementalType(
name = name,
displayName = displayName,
hue = hue,
- textureXMultiplier = textureXMultiplier
+ textureXMultiplier = textureXMultiplier,
+ primaryColor = primaryColor,
+ secondaryColor = secondaryColor
).also {
allTypes.add(it)
}
@@ -273,4 +311,4 @@ object ElementalTypes {
@JvmName("getFAIRY")
@Deprecated("Use ElementalTypes.FAIRY, provided for backwards compatibility until Cobblemon 1.8.")
fun getFAIRY() = FAIRY
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/GrowableStoneBlock.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/GrowableStoneBlock.kt
index 4fc762ea54..42ad44a82c 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/block/GrowableStoneBlock.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/GrowableStoneBlock.kt
@@ -21,6 +21,7 @@ import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.DirectionalBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
+import net.minecraft.world.level.block.state.properties.Property
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.VoxelShape
@@ -42,6 +43,8 @@ abstract class GrowableStoneBlock(
val nextStage: Block?
) : DirectionalBlock(settings) {
+ abstract val growthChance: Int
+
private val upShape: VoxelShape = Block.box(
xzOffset.toDouble(),
0.0,
@@ -95,20 +98,26 @@ abstract class GrowableStoneBlock(
.setValue(FACING, Direction.DOWN))
}
- abstract fun canGrow(pos: BlockPos, world: BlockGetter): Boolean
+ abstract fun canGrow(state: BlockState, pos: BlockPos, world: BlockGetter): Boolean
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(FACING)
}
- override fun isRandomlyTicking(state: BlockState): Boolean = stage < MAX_STAGE
-
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
- if (world.random.nextInt(5) == 0 && canGrow(pos, world)) {
+ if (world.random.nextInt(growthChance) == 0 && canGrow(state, pos, world)) {
val block = nextStage
if (block != null) {
- val newState = block.defaultBlockState().setValue(FACING, state.getValue(FACING)) as BlockState
+ var newState = block.defaultBlockState()
+
+ @Suppress("UNCHECKED_CAST")
+ for (property in state.properties) {
+ if (newState.hasProperty(property)) {
+ newState = newState.setValue(property as Property<Comparable<Any>>, state.getValue(property) as Comparable<Any>)
+ }
+ }
+
world.setBlockAndUpdate(pos, newState)
}
}
@@ -155,14 +164,4 @@ abstract class GrowableStoneBlock(
else -> upShape
}
}
-
- companion object {
- const val STAGE_0 = 0
- const val STAGE_1 = 1
- const val STAGE_2 = 2
- const val STAGE_3 = 3
-
- const val MAX_STAGE = STAGE_3
- const val MIN_STAGE = STAGE_0
- }
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/MonitorBlock.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/MonitorBlock.kt
index e95592addb..e459b31519 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/block/MonitorBlock.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/MonitorBlock.kt
@@ -16,6 +16,11 @@ import com.mojang.serialization.MapCodec
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.util.StringRepresentable
+import net.minecraft.world.InteractionHand
+import net.minecraft.world.InteractionResult
+import net.minecraft.world.ItemInteractionResult
+import net.minecraft.world.entity.player.Player
+import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
@@ -28,6 +33,7 @@ import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING
import net.minecraft.world.level.block.state.properties.EnumProperty
import net.minecraft.world.level.pathfinder.PathComputationType
+import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
@@ -35,7 +41,8 @@ import net.minecraft.world.phys.shapes.VoxelShape
class MonitorBlock(settings: Properties) : MultiblockBlock(settings) {
init {
registerDefaultState(stateDefinition.any()
- .setValue(HORIZONTAL_FACING, Direction.NORTH))
+ .setValue(HORIZONTAL_FACING, Direction.NORTH)
+ .setValue(SCREEN, MonitorScreen.OFF))
}
override fun createMultiBlockEntity(
@@ -55,6 +62,47 @@ class MonitorBlock(settings: Properties) : MultiblockBlock(settings) {
return defaultBlockState().setValue(HORIZONTAL_FACING, blockPlaceContext.horizontalDirection)
}
+ override fun useWithoutItem(
+ state: BlockState,
+ level: Level,
+ pos: BlockPos,
+ player: Player,
+ hit: BlockHitResult
+ ): InteractionResult {
+ val entity = level.getBlockEntity(pos) as? FossilMultiblockEntity ?: return InteractionResult.PASS
+ return entity.handleUseWithoutItem(state, level, pos, player)
+ }
+
+ override fun useItemOn(
+ stack: ItemStack,
+ state: BlockState,
+ level: Level,
+ pos: BlockPos,
+ player: Player,
+ hand: InteractionHand,
+ hit: BlockHitResult
+ ): ItemInteractionResult {
+ val entity = level.getBlockEntity(pos) as? FossilMultiblockEntity
+ ?: return ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION
+ val result = entity.handleUseItem(stack, state, level, pos, player, hand)
+ if (result == ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION) {
+ return if (entity.handleUseWithoutItem(state, level, pos, player).consumesAction()) {
+ ItemInteractionResult.sidedSuccess(level.isClientSide)
+ } else {
+ ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION
+ }
+ }
+ return result
+ }
+
+ override fun onRemove(state: BlockState, level: Level, pos: BlockPos, newState: BlockState, movedByPiston: Boolean) {
+ if (state.block != newState.block && !level.isClientSide && !movedByPiston) {
+ val entity = level.getBlockEntity(pos) as? FossilMultiblockEntity
+ entity?.dropDisk(level, pos, state)
+ }
+ super.onRemove(state, level, pos, newState, movedByPiston)
+ }
+
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(HORIZONTAL_FACING)
builder.add(SCREEN)
@@ -143,8 +191,27 @@ class MonitorBlock(settings: Properties) : MultiblockBlock(settings) {
BLUE_PROGRESS_7,
BLUE_PROGRESS_8,
BLUE_PROGRESS_9,
- GREEN_PROGRESS_9;
+ GREEN_PROGRESS_9,
+ TM_NORMAL,
+ TM_FIRE,
+ TM_WATER,
+ TM_GRASS,
+ TM_ELECTRIC,
+ TM_ICE,
+ TM_FIGHTING,
+ TM_POISON,
+ TM_GROUND,
+ TM_FLYING,
+ TM_PSYCHIC,
+ TM_BUG,
+ TM_ROCK,
+ TM_GHOST,
+ TM_DRAGON,
+ TM_DARK,
+ TM_STEEL,
+ TM_FAIRY,
+ MUSIC;
override fun getSerializedName(): String = this.name.lowercase()
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/TMShelfBlock.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/TMShelfBlock.kt
new file mode 100644
index 0000000000..0d2c2d14e6
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/TMShelfBlock.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.block.entity.TMShelfBlockEntity
+import com.mojang.serialization.MapCodec
+import net.minecraft.core.BlockPos
+import net.minecraft.core.Direction
+import net.minecraft.world.InteractionHand
+import net.minecraft.world.InteractionResult
+import net.minecraft.world.ItemInteractionResult
+import net.minecraft.world.entity.player.Player
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.context.BlockPlaceContext
+import net.minecraft.world.level.BlockGetter
+import net.minecraft.world.level.Level
+import net.minecraft.world.level.block.*
+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.level.block.state.properties.BooleanProperty
+import net.minecraft.world.level.block.state.properties.DirectionProperty
+import net.minecraft.world.Containers
+import net.minecraft.world.phys.BlockHitResult
+import net.minecraft.world.phys.shapes.VoxelShape
+
+class TMShelfBlock(properties: Properties) : BaseEntityBlock(properties) {
+ companion object {
+ val CODEC: MapCodec<TMShelfBlock> = simpleCodec(::TMShelfBlock)
+ val FACING: DirectionProperty = HorizontalDirectionalBlock.FACING
+ val SLOT_OCCUPIED_PROPERTIES = (0 until 14).map {
+ BooleanProperty.create("slot_${it}_occupied")
+ }
+ }
+
+ init {
+ val baseState = stateDefinition.any().setValue(FACING, Direction.NORTH)
+ registerDefaultState(SLOT_OCCUPIED_PROPERTIES.fold(baseState) { state, prop ->
+ state.setValue(prop, false)
+ })
+ }
+
+ override fun codec() = CODEC
+
+ override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity {
+ return TMShelfBlockEntity(pos, state)
+ }
+
+ override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
+ builder.add(FACING)
+ SLOT_OCCUPIED_PROPERTIES.forEach { builder.add(it) }
+ }
+
+ override fun getRenderShape(state: BlockState): RenderShape = RenderShape.MODEL
+
+ override fun getStateForPlacement(ctx: BlockPlaceContext): BlockState {
+ return defaultBlockState().setValue(FACING, ctx.horizontalDirection.opposite)
+ }
+
+ override fun useWithoutItem(
+ state: BlockState, level: Level, pos: BlockPos,
+ player: Player, hit: BlockHitResult
+ ): InteractionResult {
+ val entity = level.getBlockEntity(pos) as? TMShelfBlockEntity ?: return InteractionResult.PASS
+ return entity.handleUseWithoutItem(state, level, pos, player, hit)
+ }
+
+ override fun useItemOn(
+ stack: ItemStack, state: BlockState, level: Level, pos: BlockPos,
+ player: Player, hand: InteractionHand, hit: BlockHitResult
+ ): ItemInteractionResult {
+ val entity = level.getBlockEntity(pos) as? TMShelfBlockEntity ?: return ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION
+ return entity.handleUseItem(stack, state, level, pos, player, hit)
+ }
+
+ override fun hasAnalogOutputSignal(state: BlockState) = true
+
+ override fun getAnalogOutputSignal(state: BlockState, level: Level, pos: BlockPos): Int {
+ val entity = level.getBlockEntity(pos) as? TMShelfBlockEntity
+ return entity?.items?.count { !it.isEmpty }?.coerceIn(0, 14) ?: 0
+ }
+
+ override fun onRemove(state: BlockState, level: Level, pos: BlockPos, newState: BlockState, movedByPiston: Boolean) {
+ if (!state.`is`(newState.block)) {
+ if (!level.isClientSide && !movedByPiston) {
+ val entity = level.getBlockEntity(pos) as? TMShelfBlockEntity
+ if (entity != null) {
+ for (index in entity.items.indices) {
+ val stack = entity.items[index]
+ if (!stack.isEmpty) {
+ Containers.dropItemStack(level, pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, stack)
+ entity.items[index] = ItemStack.EMPTY
+ }
+ }
+ level.updateNeighbourForOutputSignal(pos, this)
+ }
+ }
+ super.onRemove(state, level, pos, newState, movedByPiston)
+ } else {
+ super.onRemove(state, level, pos, newState, movedByPiston)
+ }
+ }
+
+ override fun getShape(
+ state: BlockState, level: BlockGetter,
+ pos: BlockPos, context: net.minecraft.world.phys.shapes.CollisionContext
+ ): VoxelShape = Block.box(0.0, 0.0, 0.0, 16.0, 16.0, 16.0)
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/TumblestoneBlock.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/TumblestoneBlock.kt
index 2639aba852..4e2cf99559 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/block/TumblestoneBlock.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/TumblestoneBlock.kt
@@ -44,15 +44,25 @@ class TumblestoneBlock(
PrimitiveCodec.INT.fieldOf("xzOffset").forGetter { it.xzOffset },
Block.CODEC.fieldOf("nextStage").forGetter { it.nextStage }
).apply(it, ::TumblestoneBlock) }
+
+ const val STAGE_0 = 0
+ const val STAGE_1 = 1
+ const val STAGE_2 = 2
+ const val STAGE_3 = 3
+
+ const val MAX_STAGE = STAGE_3
+ const val MIN_STAGE = STAGE_0
}
+ override val growthChance = 5
+
init {
registerDefaultState(stateDefinition.any()
.setValue(FACING, Direction.DOWN)
.setValue(WATERLOGGED, false))
}
- override fun canGrow(pos: BlockPos, world: BlockGetter): Boolean {
+ override fun canGrow(state: BlockState, pos: BlockPos, world: BlockGetter): Boolean {
if (stage == MAX_STAGE) return false
val iterator: Iterator<BlockPos> =
BlockPos.betweenClosed(pos.offset(-1, -1, -1), pos.offset(1, 1, 1))
@@ -66,6 +76,8 @@ class TumblestoneBlock(
return true
}
+ override fun isRandomlyTicking(state: BlockState): Boolean = stage < MAX_STAGE
+
override fun codec(): MapCodec<out DirectionalBlock> {
return CODEC
}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/TypeGemBlock.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/TypeGemBlock.kt
new file mode 100644
index 0000000000..0580048453
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/TypeGemBlock.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.CobblemonBlocks
+import com.cobblemon.mod.common.CobblemonItems
+import net.minecraft.world.item.Item
+import net.minecraft.world.level.block.Block
+
+class TypeGemBlock(
+ settings: Properties
+) : Block(settings) {
+
+ companion object {
+
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/TypeGemClusterBlock.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/TypeGemClusterBlock.kt
new file mode 100644
index 0000000000..a142ce7d1a
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/TypeGemClusterBlock.kt
@@ -0,0 +1,284 @@
+/*
+ * 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.CobblemonItems
+import com.cobblemon.mod.common.api.tags.CobblemonBlockTags
+import com.mojang.serialization.MapCodec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import net.minecraft.core.BlockPos
+import net.minecraft.core.Direction
+import net.minecraft.core.component.DataComponents
+import net.minecraft.core.registries.BuiltInRegistries
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.server.level.ServerLevel
+import net.minecraft.util.RandomSource
+import net.minecraft.world.item.BlockItem
+import net.minecraft.world.item.Item
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import net.minecraft.world.item.component.BlockItemStateProperties
+import net.minecraft.world.item.component.CustomModelData
+import net.minecraft.world.item.context.BlockPlaceContext
+import net.minecraft.world.item.enchantment.Enchantments
+import net.minecraft.world.item.enchantment.ItemEnchantments
+import net.minecraft.world.level.LevelAccessor
+import net.minecraft.world.level.LevelReader
+import net.minecraft.world.level.block.AirBlock
+import net.minecraft.world.level.block.Block
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.world.level.block.DirectionalBlock
+import net.minecraft.world.level.block.Mirror
+import net.minecraft.world.level.block.Rotation
+import net.minecraft.world.level.block.state.BlockState
+import net.minecraft.world.level.block.state.StateDefinition
+import net.minecraft.world.level.block.state.properties.BooleanProperty
+import net.minecraft.world.level.block.state.properties.DirectionProperty
+import net.minecraft.world.level.block.state.properties.IntegerProperty
+import net.minecraft.world.level.material.PushReaction
+import net.minecraft.world.level.storage.loot.LootContext
+import net.minecraft.world.level.storage.loot.LootParams
+import net.minecraft.world.level.storage.loot.parameters.LootContextParams
+import net.minecraft.world.phys.shapes.Shapes
+import net.minecraft.world.phys.shapes.VoxelShape
+import javax.swing.text.html.HTML.Attribute.SHAPES
+import kotlin.io.path.Path
+
+class TypeGemClusterBlock(
+ settings: Properties,
+ val nextStage: Block,
+ val dropItemId: ResourceLocation
+) : DirectionalBlock(settings.pushReaction(PushReaction.DESTROY)) {
+
+ companion object {
+ val CODEC: MapCodec<TypeGemClusterBlock> = RecordCodecBuilder.mapCodec { instance ->
+ instance.group(
+ propertiesCodec(),
+ Block.CODEC.fieldOf("nextStage").forGetter { it.nextStage },
+ ResourceLocation.CODEC.fieldOf("dropItem").forGetter { it.dropItemId }
+ ).apply(instance) { settings, nextStage, dropItemId ->
+ TypeGemClusterBlock(settings, nextStage, dropItemId).also {
+ gemToClusterMap[nextStage] = it
+ }
+ }
+ }
+ val gemToClusterMap: MutableMap<Block, TypeGemClusterBlock> = mutableMapOf()
+
+ val SHOULD_GROW: BooleanProperty = BooleanProperty.create("should_grow")
+ val FACING: DirectionProperty = DirectionalBlock.FACING
+ val STAGE: IntegerProperty = IntegerProperty.create("stage", 0, 3)
+ val STUNTED: BooleanProperty = BooleanProperty.create("stunted")
+
+ fun clusterFromGemBlock(gemBlock: Block): TypeGemClusterBlock? {
+ return gemToClusterMap[gemBlock]
+ }
+ }
+
+ init {
+ registerDefaultState(stateDefinition.any()
+ .setValue(FACING, Direction.UP)
+ .setValue(SHOULD_GROW, true)
+ .setValue(STAGE, 0)
+ .setValue(STUNTED, false)
+ )
+ }
+
+ override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
+ val facing = context.clickedFace
+ val world = context.level
+ val pos = context.clickedPos.relative(facing.opposite)
+ val blockBelow = world.getBlockState(pos).block
+ val shouldGrow = blockBelow is TypeGemCoreBlock
+
+ return defaultBlockState()
+ .setValue(FACING, facing)
+ .setValue(SHOULD_GROW, shouldGrow)
+ .setValue(STAGE, 0)
+ .setValue(STUNTED, false)
+ }
+
+ override fun isRandomlyTicking(state: BlockState): Boolean = state.getValue(SHOULD_GROW)
+
+ override fun randomTick(state: BlockState, level: ServerLevel, pos: BlockPos, random: RandomSource) {
+ advanceGrowth(state, level, pos, random)
+ }
+
+ fun advanceGrowth(state: BlockState, level: LevelAccessor, pos: BlockPos, random: RandomSource) {
+ if (!state.getValue(SHOULD_GROW)) return
+
+ val currentStage = state.getValue(STAGE)
+
+ if (currentStage < 3) {
+ // Progress through stages regardless of STUNTED
+ level.setBlock(pos, state.setValue(STAGE, currentStage + 1), Block.UPDATE_ALL)
+ return
+ }
+
+ // At STAGE_3
+ val isStunted = state.getValue(STUNTED)
+ val facing = state.getValue(FACING)
+
+ if (isStunted) {
+ // println("[TypeGemClusterBlock] Cluster at $pos is STUNTED at STAGE_3. Finalizing growth.")
+ level.setBlock(pos, state.setValue(SHOULD_GROW, false), Block.UPDATE_ALL)
+ return
+ }
+
+ // Log type of cluster based on nextStage
+ val clusterType = level.registryAccess()
+ .registryOrThrow(BuiltInRegistries.BLOCK.key())
+ .getKey(nextStage)
+ .toString()
+
+ // println("[TypeGemClusterBlock] Attempting to grow into TypeGemBlock at $pos")
+ // println("[TypeGemClusterBlock] Cluster type: $clusterType")
+
+ // Check neighboring gem blocks
+ val hasConflict = Direction.entries
+ .filter { it != facing.opposite }
+ .any { dir ->
+ val neighborPos = pos.relative(dir)
+ val neighborState = level.getBlockState(neighborPos)
+ val neighborBlockId = BuiltInRegistries.BLOCK.getKey(neighborState.block)
+ // println("[TypeGemClusterBlock] Neighbor at $dir -> $neighborBlockId")
+ isGem(neighborState)
+ }
+
+ if (hasConflict) {
+ // println("[TypeGemClusterBlock] Found nearby TypeGemBlock(s). Stunting cluster at $pos.")
+ level.setBlock(
+ pos,
+ state.setValue(STUNTED, true).setValue(SHOULD_GROW, false),
+ Block.UPDATE_ALL
+ )
+ } else {
+ // println("[TypeGemClusterBlock] No conflicts. Converting cluster to TypeGemBlock.")
+ var nextState = nextStage.defaultBlockState()
+ if (nextState.hasProperty(FACING)) {
+ nextState = nextState.setValue(FACING, facing)
+ }
+ level.setBlock(pos, nextState, Block.UPDATE_ALL)
+ attachDecorativeClusters(level, pos, random)
+ }
+ }
+
+
+ private fun isGem(state: BlockState): Boolean {
+ return state.`is`(CobblemonBlockTags.TYPE_GEM_BLOCKS)
+ }
+
+ private fun attachDecorativeClusters(level: LevelAccessor, gemPos: BlockPos, random: RandomSource) {
+ val openDirections = Direction.entries.filter { direction ->
+ level.getBlockState(gemPos.relative(direction)).isAir
+ }.toMutableList()
+
+ if (openDirections.isEmpty()) {
+ return
+ }
+
+ val minClusters = maxOf(1, (openDirections.size * 0.75f).toInt())
+ val clusterCount = random.nextInt(minClusters, openDirections.size + 1)
+
+ repeat(clusterCount) {
+ if (openDirections.isEmpty()) return@repeat
+
+ val index = random.nextInt(openDirections.size)
+ val direction = openDirections.removeAt(index)
+ val clusterPos = gemPos.relative(direction)
+ val stage = random.nextInt(0, 4)
+
+ val clusterState = defaultBlockState()
+ .setValue(FACING, direction)
+ .setValue(STAGE, stage)
+ .setValue(SHOULD_GROW, false) // so we can reduce lag... hopefully
+ .setValue(STUNTED, false)
+
+ level.setBlock(clusterPos, clusterState, Block.UPDATE_ALL)
+ }
+ }
+
+ override fun getDrops(state: BlockState, params: LootParams.Builder): List<ItemStack> {
+ val drops = super.getDrops(state, params)
+
+ for (stack in drops) {
+ val blockItem = stack.item as? BlockItem ?: continue
+ if (blockItem.block !is TypeGemClusterBlock) continue
+
+ stack.set(
+ DataComponents.BLOCK_STATE,
+ BlockItemStateProperties.EMPTY
+ .with(STAGE, state.getValue(STAGE))
+ .with(SHOULD_GROW, false)
+ .with(STUNTED, state.getValue(STUNTED))
+ )
+ stack.set(
+ DataComponents.CUSTOM_MODEL_DATA,
+ CustomModelData(state.getValue(STAGE))
+ )
+ }
+
+ return drops
+ }
+
+ override fun getShape(
+ state: BlockState,
+ level: net.minecraft.world.level.BlockGetter,
+ pos: BlockPos,
+ context: net.minecraft.world.phys.shapes.CollisionContext
+ ): VoxelShape {
+ val stage = state.getValue(STAGE)
+ val facing = state.getValue(FACING)
+
+ val (min, max) = when (stage) {
+ 0 -> 5.0 to 11.0
+ 1 -> 4.0 to 12.0
+ 2 -> 3.0 to 13.0
+ 3 -> 2.0 to 14.0
+ else -> return Shapes.empty()
+ }
+
+ return when (facing) {
+ Direction.UP -> Shapes.box(min / 16, 0.0, min / 16, max / 16, (min + 1) / 16, max / 16)
+ Direction.DOWN -> Shapes.box(min / 16, (16 - (min + 1)) / 16, min / 16, max / 16, 1.0, max / 16)
+ Direction.NORTH -> Shapes.box(min / 16, min / 16, (16 - (min + 1)) / 16, max / 16, max / 16, 1.0)
+ Direction.SOUTH -> Shapes.box((16 - max) / 16, min / 16, 0.0, (16 - min) / 16, max / 16, (min + 1) / 16)
+ Direction.WEST -> Shapes.box((16 - (min + 1)) / 16, min / 16, min / 16, 1.0, max / 16, max / 16)
+ Direction.EAST -> Shapes.box(0.0, min / 16, min / 16, (min + 1) / 16, max / 16, max / 16)
+ }
+ }
+
+ override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, level: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
+ val facing = state.getValue(FACING)
+ if (direction == facing.opposite && !canSurvive(state, level, pos)) {
+ return Blocks.AIR.defaultBlockState()
+ }
+ return super.updateShape(state, direction, neighborState, level, pos, neighborPos)
+ }
+
+ override fun rotate(state: BlockState, rotation: Rotation): BlockState {
+ return state.setValue(FACING, rotation.rotate(state.getValue(FACING)))
+ }
+
+ override fun mirror(state: BlockState, mirror: Mirror): BlockState {
+ return state.rotate(mirror.getRotation(state.getValue(FACING)))
+ }
+
+ override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
+ val supportPos = pos.relative(state.getValue(FACING).opposite)
+ val supportBlock = level.getBlockState(supportPos).block
+ return supportBlock !is AirBlock
+ }
+
+ override fun codec(): MapCodec<out DirectionalBlock> = CODEC
+
+ override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
+ super.createBlockStateDefinition(builder)
+ builder.add(FACING, SHOULD_GROW, STAGE, STUNTED)
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/TypeGemCoreBlock.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/TypeGemCoreBlock.kt
new file mode 100644
index 0000000000..c791841741
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/TypeGemCoreBlock.kt
@@ -0,0 +1,215 @@
+/*
+ * 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.CobblemonBlocks
+import com.cobblemon.mod.common.api.tags.CobblemonBlockTags
+import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.SHOULD_GROW
+import com.cobblemon.mod.common.block.TypeGemClusterBlock.Companion.STUNTED
+import net.minecraft.core.BlockPos
+import net.minecraft.core.Direction
+import net.minecraft.core.registries.BuiltInRegistries
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.server.level.ServerLevel
+import net.minecraft.util.RandomSource
+import net.minecraft.world.level.BlockGetter
+import net.minecraft.world.level.LevelAccessor
+import net.minecraft.world.level.WorldGenLevel
+import net.minecraft.world.level.block.Block
+import net.minecraft.world.level.block.DirectionalBlock
+import net.minecraft.world.level.block.state.BlockState
+import java.util.*
+
+class TypeGemCoreBlock(properties: Properties) : Block(properties) {
+
+ companion object {
+ const val MAX_CONNECTED_GEMS = 7
+ const val MIN_DISTANCE_BETWEEN_GEMS = 1
+ const val CONTINUATION_CHANCE = 0.8f
+
+ val BLOCK_TO_CLUSTER: Map<ResourceLocation, Block> by lazy {
+ mapOf(
+ ResourceLocation.parse("cobblemon:type_gem_block_normal") to CobblemonBlocks.TYPE_GEM_CLUSTER_NORMAL,
+ ResourceLocation.parse("cobblemon:type_gem_block_fire") to CobblemonBlocks.TYPE_GEM_CLUSTER_FIRE,
+ ResourceLocation.parse("cobblemon:type_gem_block_water") to CobblemonBlocks.TYPE_GEM_CLUSTER_WATER,
+ ResourceLocation.parse("cobblemon:type_gem_block_electric") to CobblemonBlocks.TYPE_GEM_CLUSTER_ELECTRIC,
+ ResourceLocation.parse("cobblemon:type_gem_block_grass") to CobblemonBlocks.TYPE_GEM_CLUSTER_GRASS,
+ ResourceLocation.parse("cobblemon:type_gem_block_ice") to CobblemonBlocks.TYPE_GEM_CLUSTER_ICE,
+ ResourceLocation.parse("cobblemon:type_gem_block_fighting") to CobblemonBlocks.TYPE_GEM_CLUSTER_FIGHTING,
+ ResourceLocation.parse("cobblemon:type_gem_block_poison") to CobblemonBlocks.TYPE_GEM_CLUSTER_POISON,
+ ResourceLocation.parse("cobblemon:type_gem_block_ground") to CobblemonBlocks.TYPE_GEM_CLUSTER_GROUND,
+ ResourceLocation.parse("cobblemon:type_gem_block_flying") to CobblemonBlocks.TYPE_GEM_CLUSTER_FLYING,
+ ResourceLocation.parse("cobblemon:type_gem_block_psychic") to CobblemonBlocks.TYPE_GEM_CLUSTER_PSYCHIC,
+ ResourceLocation.parse("cobblemon:type_gem_block_bug") to CobblemonBlocks.TYPE_GEM_CLUSTER_BUG,
+ ResourceLocation.parse("cobblemon:type_gem_block_rock") to CobblemonBlocks.TYPE_GEM_CLUSTER_ROCK,
+ ResourceLocation.parse("cobblemon:type_gem_block_ghost") to CobblemonBlocks.TYPE_GEM_CLUSTER_GHOST,
+ ResourceLocation.parse("cobblemon:type_gem_block_dragon") to CobblemonBlocks.TYPE_GEM_CLUSTER_DRAGON,
+ ResourceLocation.parse("cobblemon:type_gem_block_dark") to CobblemonBlocks.TYPE_GEM_CLUSTER_DARK,
+ ResourceLocation.parse("cobblemon:type_gem_block_steel") to CobblemonBlocks.TYPE_GEM_CLUSTER_STEEL,
+ ResourceLocation.parse("cobblemon:type_gem_block_fairy") to CobblemonBlocks.TYPE_GEM_CLUSTER_FAIRY
+ )
+ }
+
+ fun setStuntState(level: LevelAccessor, gemBlocks: List<Pair<BlockState, BlockPos>>, stunted: Boolean) {
+ for ((_, gemPos) in gemBlocks) {
+ for (dir in Direction.entries) {
+ val neighborPos = gemPos.relative(dir)
+ val neighborState = level.getBlockState(neighborPos)
+
+ if (neighborState.block is TypeGemClusterBlock) {
+ if (neighborState.hasProperty(STUNTED) && neighborState.getValue(STUNTED) != stunted) {
+
+ var updatedState = neighborState.setValue(STUNTED, stunted)
+
+ if (updatedState.hasProperty(SHOULD_GROW)) {
+ updatedState = updatedState.setValue(SHOULD_GROW, !stunted)
+ }
+
+ level.setBlock(neighborPos, updatedState, 3)
+ }
+ }
+ }
+ }
+ }
+
+ fun getConnectedGemBlocks(level: BlockGetter, pos: BlockPos): List<Pair<BlockState, BlockPos>> {
+ val connectedGems = mutableListOf<Pair<BlockState, BlockPos>>()
+ val visited = mutableSetOf<BlockPos>()
+ val queue: Queue<BlockPos> = LinkedList()
+
+ visited.add(pos)
+ queue.add(pos)
+ connectedGems.add(level.getBlockState(pos) to pos)
+
+ while (queue.isNotEmpty()) {
+ val current = queue.poll()
+
+ for (direction in Direction.entries) {
+ val neighbor = current.relative(direction)
+ if (visited.add(neighbor)) {
+ val state = level.getBlockState(neighbor)
+ if (state.`is`(CobblemonBlockTags.TYPE_GEM_BLOCKS)) {
+ connectedGems.add(state to neighbor)
+ queue.add(neighbor)
+ }
+ }
+ }
+ }
+ return connectedGems
+ }
+ }
+
+ override fun randomTick(state: BlockState, level: ServerLevel, pos: BlockPos, random: RandomSource) {
+ grow(level, pos, random)
+ }
+
+ fun forceGrow(level: WorldGenLevel, pos: BlockPos, random: RandomSource, percentage: Float) {
+ val desiredLength = MAX_CONNECTED_GEMS * percentage
+
+ while (true) {
+ val growInfo = grow(level, pos, random, true)
+ val grown = growInfo.first
+ val clusterSize = growInfo.second
+
+ if (!grown || clusterSize >= desiredLength) break
+ }
+ }
+
+ private fun grow(
+ level: WorldGenLevel,
+ pos: BlockPos,
+ random: RandomSource,
+ forced: Boolean = false
+ ): Pair<Boolean, Int> {
+ val connectedGems = getConnectedGemBlocks(level, pos) // maybe we can store this and access it for better performance? check for changes in list rather than checking each time grow is called? idk
+ val gemCount = connectedGems.size
+ val overLimit = gemCount >= MAX_CONNECTED_GEMS
+
+ if (overLimit) {
+ // if there is no open air pockets to grow any new clusters then exit early
+ if (!hasBreathingRoom(level, connectedGems)) {
+ return Pair(false, gemCount)
+ }
+ // max type gem block count met, tell clusters to grow but not turn into blocks
+ setStuntState(level, connectedGems, true)
+ //return Pair(false, gemCount)
+ } else {
+ // make sure connected clusters know they can grow and turn into blocks
+ setStuntState(level, connectedGems, false)
+ }
+
+ val javaRandom = java.util.Random(random.nextLong())
+
+ for ((gemState, gemPos) in connectedGems.shuffled(javaRandom)) {
+ val registryKey = BuiltInRegistries.BLOCK.getKey(gemState.block)
+ val clusterBlock = BLOCK_TO_CLUSTER[registryKey]
+
+ for (dir in Direction.entries.shuffled(javaRandom)) {
+ val targetPos = gemPos.relative(dir)
+ val targetState = level.getBlockState(targetPos)
+
+ if (!targetState.isAir) continue
+ if (!isPositionValidForGrowth(level, targetPos)) continue
+
+ // Place gem cluster
+ if (clusterBlock != null) {
+ var placeState = clusterBlock.defaultBlockState()
+ .setValue(DirectionalBlock.FACING, dir)
+ .setValue(TypeGemClusterBlock.STAGE, 0)
+ .setValue(SHOULD_GROW, true)
+
+ level.setBlock(targetPos, placeState, UPDATE_ALL)
+ }
+
+ if (forced && clusterBlock != null) {
+ forceAdvanceClusterGrowth(level, targetPos, random)
+ }
+
+ val updatedGemCount = if (forced) getConnectedGemBlocks(level, pos).size else connectedGems.size
+ return Pair(true, updatedGemCount)
+ }
+ }
+
+ // println("[TypeGemCoreBlock] No valid spot found for gem placement.")
+ return Pair(false, connectedGems.size)
+ }
+
+ private fun forceAdvanceClusterGrowth(level: WorldGenLevel, clusterPos: BlockPos, random: RandomSource) {
+ repeat(5) {
+ val clusterState = level.getBlockState(clusterPos)
+ val clusterBlock = clusterState.block as? TypeGemClusterBlock ?: return
+ clusterBlock.advanceGrowth(clusterState, level, clusterPos, random)
+ }
+ }
+
+ private fun hasBreathingRoom(level: WorldGenLevel, gems: List<Pair<BlockState, BlockPos>>): Boolean {
+ for ((_, gemPos) in gems) {
+ for (dir in Direction.entries) {
+ if (level.getBlockState(gemPos.relative(dir)).isAir) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ private fun isPositionValidForGrowth(level: WorldGenLevel, pos: BlockPos): Boolean {
+ return Direction.entries.none { dir ->
+ val nearby = pos.relative(dir)
+ isGem(level.getBlockState(nearby)) &&
+ pos.distManhattan(nearby) < MIN_DISTANCE_BETWEEN_GEMS
+ }
+ }
+
+ private fun isGem(state: BlockState): Boolean {
+ return state.`is`(CobblemonBlockTags.TYPE_GEM_BLOCKS)
+ }
+
+ override fun isRandomlyTicking(state: BlockState): Boolean = true
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/FossilMultiblockEntity.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/FossilMultiblockEntity.kt
index c34a7e78b8..307393c80a 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/FossilMultiblockEntity.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/FossilMultiblockEntity.kt
@@ -12,15 +12,35 @@ import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.api.multiblock.MultiblockEntity
import com.cobblemon.mod.common.api.multiblock.MultiblockStructure
import com.cobblemon.mod.common.api.multiblock.builder.MultiblockStructureBuilder
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.api.types.ElementalTypes
+import com.cobblemon.mod.common.block.MonitorBlock
import com.cobblemon.mod.common.block.multiblock.FossilMultiblockStructure
+import com.cobblemon.mod.common.item.components.TMMoveComponent
+import com.cobblemon.mod.common.item.interactive.TechnicalMachineItem
import com.cobblemon.mod.common.util.DataKeys
+import com.cobblemon.mod.common.util.tmList
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.CompoundTag
+import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtUtils
+import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.level.ChunkPos
+import net.minecraft.world.Containers
+import net.minecraft.world.InteractionResult
+import net.minecraft.world.ItemInteractionResult
+import net.minecraft.world.InteractionHand
+import net.minecraft.world.entity.item.ItemEntity
+import net.minecraft.world.entity.player.Player
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
+import net.minecraft.world.level.Level
+import net.minecraft.world.level.block.HorizontalDirectionalBlock
+import net.minecraft.world.level.block.state.properties.BlockStateProperties
+import net.minecraft.world.level.gameevent.GameEvent
open class FossilMultiblockEntity(
pos: BlockPos,
@@ -28,8 +48,30 @@ open class FossilMultiblockEntity(
multiblockBuilder: MultiblockStructureBuilder,
type: BlockEntityType<*> = CobblemonBlockEntities.FOSSIL_MULTIBLOCK
) : MultiblockEntity(type, pos, state, multiblockBuilder) {
+ private val musicDiscs = setOf(
+ Items.MUSIC_DISC_13,
+ Items.MUSIC_DISC_CAT,
+ Items.MUSIC_DISC_BLOCKS,
+ Items.MUSIC_DISC_CHIRP,
+ Items.MUSIC_DISC_FAR,
+ Items.MUSIC_DISC_MALL,
+ Items.MUSIC_DISC_MELLOHI,
+ Items.MUSIC_DISC_STAL,
+ Items.MUSIC_DISC_STRAD,
+ Items.MUSIC_DISC_WARD,
+ Items.MUSIC_DISC_11,
+ Items.MUSIC_DISC_WAIT,
+ Items.MUSIC_DISC_OTHERSIDE,
+ Items.MUSIC_DISC_5,
+ Items.MUSIC_DISC_RELIC,
+ Items.MUSIC_DISC_PRECIPICE,
+ Items.MUSIC_DISC_CREATOR,
+ Items.MUSIC_DISC_CREATOR_MUSIC_BOX,
+ Items.MUSIC_DISC_PIGSTEP
+ )
override var masterBlockPos: BlockPos? = null
+ private var diskStack: ItemStack = ItemStack.EMPTY
override var multiblockStructure: MultiblockStructure? = null
set(structure) {
@@ -76,6 +118,164 @@ open class FossilMultiblockEntity(
} else {
null
}
+ diskStack = if (nbt.contains(DataKeys.MONITOR_DISK)) {
+ ItemStack.parse(registryLookup, nbt.get(DataKeys.MONITOR_DISK)).orElse(ItemStack.EMPTY)
+ } else {
+ ItemStack.EMPTY
+ }
+ updateMonitorScreen()
+ }
+
+ override fun saveAdditional(nbt: CompoundTag, registryLookup: HolderLookup.Provider) {
+ super.saveAdditional(nbt, registryLookup)
+ if (!diskStack.isEmpty) {
+ nbt.put(DataKeys.MONITOR_DISK, ItemStack.CODEC.encodeStart(NbtOps.INSTANCE, diskStack).orThrow)
+ }
+ }
+
+ fun handleUseItem(
+ stack: ItemStack,
+ state: BlockState,
+ level: Level,
+ pos: BlockPos,
+ player: Player,
+ hand: InteractionHand
+ ): ItemInteractionResult {
+ val handStack = player.getItemInHand(hand)
+ if (!isValidDisk(handStack)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION
+ if (multiblockStructure != null) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION
+
+ if (level.isClientSide) return ItemInteractionResult.SUCCESS
+
+ val newDisk = handStack.copyWithCount(1)
+ // In creative, prevent duplicate ejection spam when repeatedly inserting the same disk.
+ if (player.isCreative && !diskStack.isEmpty && ItemStack.isSameItemSameComponents(diskStack, newDisk)) {
+ return ItemInteractionResult.sidedSuccess(level.isClientSide)
+ }
+
+ val oldStack = diskStack
+ if (!oldStack.isEmpty) {
+ if (player is ServerPlayer) {
+ unlockTmForPlayer(player, oldStack)
+ }
+ ejectDiskStack(level, pos, state, oldStack)
+ }
+
+ diskStack = newDisk
+ if (!player.isCreative) {
+ handStack.shrink(1)
+ }
+
+ if (player is ServerPlayer) {
+ unlockTmForPlayer(player, diskStack)
+ }
+
+ updateMonitorScreen()
+ markUpdated(level, pos, state)
+
+ return ItemInteractionResult.sidedSuccess(level.isClientSide)
+ }
+
+ fun handleUseWithoutItem(
+ state: BlockState,
+ level: Level,
+ pos: BlockPos,
+ player: Player
+ ): InteractionResult {
+ if (diskStack.isEmpty) return InteractionResult.PASS
+ if (multiblockStructure != null) return InteractionResult.PASS
+
+ if (player is ServerPlayer) {
+ unlockTmForPlayer(player, diskStack)
+ }
+ ejectDiskStack(level, pos, state, diskStack)
+ diskStack = ItemStack.EMPTY
+ updateMonitorScreen()
+ markUpdated(level, pos, state)
+
+ return InteractionResult.sidedSuccess(level.isClientSide)
+ }
+
+ fun dropDisk(level: Level, pos: BlockPos, state: BlockState) {
+ if (diskStack.isEmpty) return
+ ejectDiskStack(level, pos, state, diskStack)
+ diskStack = ItemStack.EMPTY
+ }
+
+ private fun ejectDiskStack(level: Level, pos: BlockPos, state: BlockState, stack: ItemStack) {
+ val facing = if (state.hasProperty(BlockStateProperties.HORIZONTAL_FACING)) {
+ state.getValue(HorizontalDirectionalBlock.FACING)
+ } else {
+ null
+ }
+ if (facing == null) {
+ Containers.dropItemStack(level, pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, stack)
+ return
+ }
+
+ val ejectFacing = facing.opposite
+ val x = pos.x + 0.5 + ejectFacing.stepX * 0.6
+ val y = pos.y + 0.5
+ val z = pos.z + 0.5 + ejectFacing.stepZ * 0.6
+ val itemEntity = ItemEntity(level, x, y, z, stack)
+ itemEntity.setDeltaMovement(ejectFacing.stepX * 0.15, 0.05, ejectFacing.stepZ * 0.15)
+ level.addFreshEntity(itemEntity)
+ }
+
+ private fun isValidDisk(stack: ItemStack): Boolean {
+ return stack.item is TechnicalMachineItem || musicDiscs.contains(stack.item)
+ }
+
+ private fun unlockTmForPlayer(player: ServerPlayer, stack: ItemStack) {
+ if (stack.item !is TechnicalMachineItem) return
+ val move = TMMoveComponent.getTMMove(stack) ?: return
+ val tmId = TechnicalMachines.moveToTM[move]?.id ?: return
+ player.tmList()?.learn(listOf(tmId))
+ }
+
+ private fun updateMonitorScreen() {
+ val level = level ?: return
+ if (level.isClientSide) return
+ if (multiblockStructure != null) return
+
+ val state = blockState
+ if (!state.hasProperty(MonitorBlock.SCREEN)) return
+ val targetScreen = getDiskScreen()
+ if (state.getValue(MonitorBlock.SCREEN) != targetScreen) {
+ level.setBlockAndUpdate(blockPos, state.setValue(MonitorBlock.SCREEN, targetScreen))
+ }
+ }
+
+ private fun getDiskScreen(): MonitorBlock.MonitorScreen {
+ if (diskStack.isEmpty) return MonitorBlock.MonitorScreen.OFF
+ if (musicDiscs.contains(diskStack.item)) return MonitorBlock.MonitorScreen.MUSIC
+ val move = TMMoveComponent.getTMMove(diskStack) ?: return MonitorBlock.MonitorScreen.TM_NORMAL
+ return when (move.elementalType) {
+ ElementalTypes.FIRE -> MonitorBlock.MonitorScreen.TM_FIRE
+ ElementalTypes.WATER -> MonitorBlock.MonitorScreen.TM_WATER
+ ElementalTypes.GRASS -> MonitorBlock.MonitorScreen.TM_GRASS
+ ElementalTypes.ELECTRIC -> MonitorBlock.MonitorScreen.TM_ELECTRIC
+ ElementalTypes.ICE -> MonitorBlock.MonitorScreen.TM_ICE
+ ElementalTypes.FIGHTING -> MonitorBlock.MonitorScreen.TM_FIGHTING
+ ElementalTypes.POISON -> MonitorBlock.MonitorScreen.TM_POISON
+ ElementalTypes.GROUND -> MonitorBlock.MonitorScreen.TM_GROUND
+ ElementalTypes.FLYING -> MonitorBlock.MonitorScreen.TM_FLYING
+ ElementalTypes.PSYCHIC -> MonitorBlock.MonitorScreen.TM_PSYCHIC
+ ElementalTypes.BUG -> MonitorBlock.MonitorScreen.TM_BUG
+ ElementalTypes.ROCK -> MonitorBlock.MonitorScreen.TM_ROCK
+ ElementalTypes.GHOST -> MonitorBlock.MonitorScreen.TM_GHOST
+ ElementalTypes.DRAGON -> MonitorBlock.MonitorScreen.TM_DRAGON
+ ElementalTypes.DARK -> MonitorBlock.MonitorScreen.TM_DARK
+ ElementalTypes.STEEL -> MonitorBlock.MonitorScreen.TM_STEEL
+ ElementalTypes.FAIRY -> MonitorBlock.MonitorScreen.TM_FAIRY
+ else -> MonitorBlock.MonitorScreen.TM_NORMAL
+ }
+ }
+
+ private fun markUpdated(level: Level, pos: BlockPos, state: BlockState) {
+ level.sendBlockUpdated(pos, state, state, 3)
+ level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(state))
+ setChanged()
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/TMMachineBlockEntity.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/TMMachineBlockEntity.kt
new file mode 100644
index 0000000000..09bffea5db
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/TMMachineBlockEntity.kt
@@ -0,0 +1,383 @@
+/*
+ * 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.entity
+
+import com.cobblemon.mod.common.CobblemonBlockEntities
+import com.cobblemon.mod.common.CobblemonItems
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.moves.Moves
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.block.tmmachine.TMMachineBlock
+import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
+import com.cobblemon.mod.common.client.gui.tmmachine.TMMachineScreen
+import com.cobblemon.mod.common.item.components.TMMoveComponent
+import com.cobblemon.mod.common.util.itemRegistry
+import net.minecraft.core.BlockPos
+import net.minecraft.core.HolderLookup
+import net.minecraft.core.NonNullList
+import net.minecraft.network.chat.Component
+import net.minecraft.network.protocol.Packet
+import net.minecraft.network.protocol.game.ClientGamePacketListener
+import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket
+import net.minecraft.world.ContainerHelper
+import net.minecraft.world.SimpleContainer
+import net.minecraft.world.entity.player.Inventory
+import net.minecraft.world.entity.player.Player
+import net.minecraft.world.inventory.AbstractContainerMenu
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.level.block.entity.BaseContainerBlockEntity
+import net.minecraft.world.level.block.state.BlockState
+import net.minecraft.nbt.CompoundTag
+import net.minecraft.sounds.SoundEvent
+import net.minecraft.sounds.SoundSource
+import net.minecraft.world.Container
+import net.minecraft.world.inventory.ContainerData
+import net.minecraft.world.level.Level
+
+class TMMachineBlockEntity(pos: BlockPos, state: BlockState) :
+ BaseContainerBlockEntity(CobblemonBlockEntities.TM_MACHINE, pos, state) {
+ companion object {
+ const val BURN_ACTIVE_TAG = "burnActive"
+ const val BURN_PROGRESS_TAG = "burnProgress"
+ const val REPEAT_PROCESS_TAG = "repeatProcess"
+ const val ACTIVE_MOVE_TAG = "activeMove"
+
+ const val BURN_PROGRESS_PER_TICK = 2
+ const val BURN_TOTAL_TIME = 200
+ const val TOTAL_PROCESS_TIME =
+ BURN_TOTAL_TIME + ((TMMachineScreen.CRAFT_TICKS + TMMachineScreen.RESET_DISC_TICKS) * BURN_PROGRESS_PER_TICK)
+
+ // Container data IDs
+ const val BURN_PROGRESS_INDEX = 0
+ const val BURN_ACTIVE_INDEX = 1
+ const val REPEAT_PROCESS_INDEX = 2
+ const val POSITION_X_INDEX = 3
+ const val POSITION_Y_INDEX = 4
+ const val POSITION_Z_INDEX = 5
+
+ fun serverTick(level: Level, pos: BlockPos, state: BlockState, blockEntity: TMMachineBlockEntity) {
+ if (level.isClientSide) return
+
+ // check if the TMM is powered by redstone. For toggling automation
+ val powered = level.hasNeighborSignal(pos)
+ val data = blockEntity.containerData
+
+ // Progress while active
+ if (data.get(BURN_ACTIVE_INDEX) == 1) {
+ val currentProgress = data.get(BURN_PROGRESS_INDEX)
+ if (currentProgress < TOTAL_PROCESS_TIME) {
+ val progressPerTick = if (currentProgress >= BURN_TOTAL_TIME) 1 else BURN_PROGRESS_PER_TICK
+ data.set(BURN_PROGRESS_INDEX, currentProgress + progressPerTick)
+
+ // comparator progress bar updates
+ level.updateNeighbourForOutputSignal(pos, state.block)
+ }
+ }
+
+ // auto start with batching when powered
+ if (powered && data.get(BURN_ACTIVE_INDEX) == 0) {
+ if (blockEntity.canCraftSelectedTM()) {
+ data.set(BURN_ACTIVE_INDEX, 1)
+ data.set(BURN_PROGRESS_INDEX, 0)
+ blockEntity.setChanged()
+ level.updateNeighbourForOutputSignal(pos, state.block)
+ }
+ }
+
+ val burnProgressValue = data.get(BURN_PROGRESS_INDEX)
+ val postCraftTicks = if (burnProgressValue >= BURN_TOTAL_TIME) (burnProgressValue - BURN_TOTAL_TIME) else 0
+
+ if (postCraftTicks > 0) {
+ if (postCraftTicks == TMMachineScreen.CRAFT_TICKS) {
+ blockEntity.craftTM()
+ level.updateNeighbourForOutputSignal(pos, state.block)
+ }
+
+ if (postCraftTicks >= (TMMachineScreen.CRAFT_TICKS + TMMachineScreen.RESET_DISC_TICKS)) {
+ data.set(BURN_PROGRESS_INDEX, 0)
+
+ // use batch processing while powered with redstone
+ val keepRunning = powered && blockEntity.canCraftSelectedTM()
+ data.set(BURN_ACTIVE_INDEX, if (keepRunning) 1 else 0)
+
+ blockEntity.setChanged()
+ level.updateNeighbourForOutputSignal(pos, state.block)
+ }
+ }
+ }
+ }
+
+ var tmMachineInventory = TMMachineBlockInventory(this)
+ var partialTicks: Float = 0F
+
+ var burnProgress: Int = 0
+ var burnActive: Boolean = false
+ var repeatProcess: Boolean = false
+ var activeMove: String = ""
+
+ var containerData: ContainerData = object : ContainerData {
+ override fun getCount() = 6
+ override fun get(index: Int): Int {
+ return when (index) {
+ BURN_PROGRESS_INDEX -> burnProgress
+ BURN_ACTIVE_INDEX -> if (burnActive) 1 else 0
+ REPEAT_PROCESS_INDEX -> if (repeatProcess) 1 else 0
+ POSITION_X_INDEX -> blockPos.x
+ POSITION_Y_INDEX -> blockPos.y
+ POSITION_Z_INDEX -> blockPos.z
+ else -> 0
+ }
+ }
+
+ override fun set(index: Int, value: Int) {
+ when (index) {
+ BURN_PROGRESS_INDEX -> burnProgress = value
+ BURN_ACTIVE_INDEX -> burnActive = value == 1
+ REPEAT_PROCESS_INDEX -> repeatProcess = value == 1
+ }
+ }
+
+ }
+
+ private fun canCraftSelectedTM(): Boolean {
+ val move = Moves.getByName(activeMove) ?: return false
+ val tm = TechnicalMachines.moveToTM[move] ?: return false
+
+ // We must have the Output be empty or stackable and not full
+ val resultStack = getItem(TMMachineMenu.RESULT_SLOT)
+ val crafted = ItemStack(CobblemonItems.TECHNICAL_MACHINE).also { TMMoveComponent.setTMMove(it, tm.moveName) }
+ val outputOk = resultStack.isEmpty || (
+ resultStack.count < resultStack.maxStackSize &&
+ ItemStack.isSameItemSameComponents(resultStack, crafted)
+ )
+ if (!outputOk) return false
+
+ // Blank disc slot
+ if (getItem(TMMachineMenu.BLANK_TM_SLOT).item != CobblemonItems.BLANK_TM) return false
+
+ // Ingredient slots
+ val recipes = tm.getClampedRecipe() ?: emptyList()
+ for ((index, recipe) in recipes.withIndex()) {
+ val slot = TMMachineMenu.INGREDIENT_SLOTS.first + index
+ val provided = getItem(slot)
+ if (!recipe.ingredient.test(provided) || provided.count < recipe.count) return false
+ }
+
+ return true
+ }
+
+ private fun craftTM() {
+ if (!(level?.isClientSide ?: true)) {
+ Moves.getByName(activeMove)?.let { move ->
+ TechnicalMachines.moveToTM[move]?.let { tm ->
+ val resultStack = getItem(TMMachineMenu.RESULT_SLOT)
+ if (!resultStack.isEmpty && resultStack.count >= resultStack.maxStackSize) return
+
+ // Craft the TM
+ val craftedTmStack = ItemStack(CobblemonItems.TECHNICAL_MACHINE)
+ .also { TMMoveComponent.setTMMove(it, tm.moveName) }
+
+ if (!resultStack.isEmpty && !ItemStack.isSameItemSameComponents(resultStack, craftedTmStack)) return
+
+ val blankTmStack = getItem(TMMachineMenu.BLANK_TM_SLOT)
+ if (blankTmStack.item != CobblemonItems.BLANK_TM) return
+
+ val recipes = tm.getClampedRecipe() ?: emptyList()
+ for ((index, recipe) in recipes.withIndex()) {
+ val slot = TMMachineMenu.INGREDIENT_SLOTS.first + index
+ val provided = getItem(slot)
+ if (!recipe.ingredient.test(provided) || provided.count < recipe.count) return
+ }
+
+ // Consume ingredients
+ blankTmStack.shrink(1)
+ for ((index, ingredient) in recipes.withIndex()) {
+ getItem(TMMachineMenu.INGREDIENT_SLOTS.first + index).shrink(ingredient.count)
+ }
+
+ // Add crafted TM to result slot
+ if (resultStack.isEmpty) {
+ setItem(TMMachineMenu.RESULT_SLOT, craftedTmStack)
+ } else if (resultStack.count < resultStack.maxStackSize) {
+ resultStack.grow(1)
+ }
+
+ tmMachineInventory.setChanged()
+ setChanged()
+ }
+ }
+ }
+ }
+
+ override fun createMenu(containerId: Int, inventory: Inventory): AbstractContainerMenu {
+ return TMMachineMenu(containerId, inventory, tmMachineInventory, containerData)
+ }
+
+ override fun saveAdditional(compound: CompoundTag, registries: HolderLookup.Provider) {
+ super.saveAdditional(compound, registries)
+ compound.putBoolean(BURN_ACTIVE_TAG, burnActive)
+ compound.putBoolean(REPEAT_PROCESS_TAG, repeatProcess)
+ compound.putInt(BURN_PROGRESS_TAG, burnProgress)
+ compound.putString(ACTIVE_MOVE_TAG, activeMove)
+ ContainerHelper.saveAllItems(compound, tmMachineInventory.items, registries)
+ }
+
+ override fun loadAdditional(compound: CompoundTag, registries: HolderLookup.Provider) {
+ super.loadAdditional(compound, registries)
+ burnActive = compound.getBoolean(BURN_ACTIVE_TAG)
+ repeatProcess = compound.getBoolean(REPEAT_PROCESS_TAG)
+ burnProgress = compound.getInt(BURN_PROGRESS_TAG)
+ activeMove = compound.getString(ACTIVE_MOVE_TAG)
+ ContainerHelper.loadAllItems(compound, tmMachineInventory.items, registries)
+ }
+
+ override fun getDisplayName(): Component = Component.translatable("block.cobblemon.tm_machine")
+
+ override fun getDefaultName(): Component = Component.translatable("cobblemon.container.tm_machine")
+
+ override fun getItems(): NonNullList<ItemStack> = tmMachineInventory.items
+
+ override fun setItems(items: NonNullList<ItemStack>) {
+ for (i in items.indices) {
+ tmMachineInventory.setItem(i, items[i])
+ }
+ }
+
+ override fun getUpdatePacket(): Packet<ClientGamePacketListener> = ClientboundBlockEntityDataPacket.create(this)
+
+ override fun getUpdateTag(registryLookup: HolderLookup.Provider): CompoundTag = saveWithoutMetadata(registryLookup)
+
+ override fun getContainerSize(): Int = tmMachineInventory.containerSize
+
+ override fun isEmpty(): Boolean = tmMachineInventory.isEmpty
+
+ override fun getItem(slot: Int): ItemStack = tmMachineInventory.getItem(slot)
+
+ override fun removeItem(slot: Int, amount: Int): ItemStack = tmMachineInventory.removeItem(slot, amount)
+
+ override fun removeItemNoUpdate(slot: Int): ItemStack = tmMachineInventory.removeItemNoUpdate(slot)
+
+ override fun setItem(slot: Int, stack: ItemStack) = tmMachineInventory.setItem(slot, stack)
+
+ override fun setChanged() {
+ var currentState = blockState
+ level?.getBlockState(worldPosition)?.let { state ->
+ currentState = state
+ var updated = false
+
+ if (state.hasProperty(TMMachineBlock.EMPTY)) {
+ val isEmpty = getItem(TMMachineMenu.BLANK_TM_SLOT).isEmpty
+ if (currentState.getValue(TMMachineBlock.EMPTY) != isEmpty) {
+ if (!isEmpty) playSound(CobblemonSounds.TM_MACHINE_PLACE_DISC, 0.5F)
+ currentState = currentState.setValue(TMMachineBlock.EMPTY, isEmpty)
+ updated = true
+ }
+ }
+ if (state.hasProperty(TMMachineBlock.ACTIVE)) {
+ val isActive = containerData.get(BURN_ACTIVE_INDEX) == 1
+ if (currentState.getValue(TMMachineBlock.ACTIVE) != isActive) {
+ currentState = currentState.setValue(TMMachineBlock.ACTIVE, isActive)
+ updated = true
+ }
+ }
+ if (updated) level?.setBlockAndUpdate(worldPosition, currentState)
+ }
+
+ // Notify the block entity's level that this block entity has changed
+ level?.blockEntityChanged(worldPosition)
+
+ // Mark the chunk containing this block entity as dirty, ensuring it is saved
+ level?.getChunkAt(worldPosition)?.isUnsaved = true
+
+ // Update Neighbours
+ level?.updateNeighborsAt(blockPos, currentState.block)
+ }
+
+ override fun stillValid(player: Player): Boolean = tmMachineInventory.stillValid(player)
+
+ override fun canPlaceItem(slot: Int, stack: ItemStack): Boolean = tmMachineInventory.canPlaceItem(slot, stack)
+
+ override fun canTakeItem(target: Container, slot: Int, stack: ItemStack): Boolean =
+ tmMachineInventory.canTakeItem(target, slot, stack)
+
+ fun playSound(soundEvent: SoundEvent, volume: Float = 1F, pitch: Float = 1F) {
+ level?.playSound(null, worldPosition, soundEvent, SoundSource.BLOCKS, volume, pitch)
+ }
+
+ fun isReadyToCraft(): Boolean = canCraftSelectedTM()
+
+ fun isOutputBlocked(): Boolean {
+ val level = level ?: return false
+ val move = Moves.getByName(activeMove) ?: return false
+ val tm = TechnicalMachines.moveToTM[move] ?: return false
+
+ val resultStack = getItem(TMMachineMenu.RESULT_SLOT)
+ if (resultStack.isEmpty) return false
+
+ val crafted = ItemStack(CobblemonItems.TECHNICAL_MACHINE)
+ .also { TMMoveComponent.setTMMove(it, tm.moveName) }
+
+ // If wrong TM in output OR output is full
+ if (!ItemStack.isSameItemSameComponents(resultStack, crafted)) return true
+ return resultStack.count >= resultStack.maxStackSize
+ }
+
+ class TMMachineBlockInventory(val blockEntity: TMMachineBlockEntity) : SimpleContainer(6) {
+ override fun canTakeItem(target: Container, slot: Int, stack: ItemStack) = slot == 0
+ override fun canPlaceItem(slot: Int, stack: ItemStack): Boolean {
+ val powered = blockEntity.level?.hasNeighborSignal(blockEntity.blockPos) == true
+ val allowAutomation = blockEntity.containerData.get(BURN_ACTIVE_INDEX) == 1 || powered
+
+ if (!allowAutomation) return false
+
+ Moves.getByName(blockEntity.activeMove)?.let {
+ val tm = TechnicalMachines.moveToTM[it] ?: return false
+ val item = stack.item
+
+ return when (slot) {
+ 1 -> item == CobblemonItems.BLANK_TM
+ 2, 3, 4 -> {
+ val recipe = tm.getClampedRecipe()
+ val recipeIndex = slot - 2
+ if (recipe == null || recipeIndex >= recipe.size) {
+ return false
+ }
+ recipe[recipeIndex].ingredient.test(stack)
+ }
+ else -> false
+ }
+ }
+
+ return false
+ }
+
+ override fun setChanged() {
+ super.setChanged()
+ blockEntity.setChanged()
+
+ val allowAutomation =
+ blockEntity.containerData.get(BURN_ACTIVE_INDEX) == 1 || blockEntity.containerData.get(
+ REPEAT_PROCESS_INDEX
+ ) == 1
+
+ // force comparators to refresh if inventory changes and batch mode is on
+ if (allowAutomation) {
+ blockEntity.level?.updateNeighbourForOutputSignal(blockEntity.blockPos, blockEntity.blockState.block)
+ }
+ }
+
+ override fun startOpen(player: Player) {
+ blockEntity.startOpen(player)
+ }
+
+ override fun stopOpen(player: Player) {
+ blockEntity.stopOpen(player)
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/TMShelfBlockEntity.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/TMShelfBlockEntity.kt
new file mode 100644
index 0000000000..ae2e034638
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/entity/TMShelfBlockEntity.kt
@@ -0,0 +1,302 @@
+/*
+ * 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.entity
+
+import com.cobblemon.mod.common.CobblemonBlockEntities
+import com.cobblemon.mod.common.CobblemonItems
+import com.cobblemon.mod.common.block.TMShelfBlock
+import com.cobblemon.mod.common.item.interactive.TechnicalMachineItem
+import net.minecraft.core.BlockPos
+import net.minecraft.core.Direction
+import net.minecraft.core.HolderLookup
+import net.minecraft.core.NonNullList
+import net.minecraft.nbt.CompoundTag
+import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket
+import net.minecraft.sounds.SoundEvent
+import net.minecraft.sounds.SoundSource
+import net.minecraft.world.ContainerHelper
+import net.minecraft.world.Containers
+import net.minecraft.world.InteractionResult
+import net.minecraft.world.ItemInteractionResult
+import net.minecraft.world.WorldlyContainer
+import net.minecraft.world.entity.player.Player
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+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.BlockHitResult
+import net.minecraft.world.level.gameevent.GameEvent
+import java.util.OptionalInt
+import kotlin.math.pow
+
+class TMShelfBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(CobblemonBlockEntities.TM_SHELF, pos, state), WorldlyContainer {
+ val items: NonNullList<ItemStack> = NonNullList.withSize(14, ItemStack.EMPTY)
+ var lastInteractedSlot: Int = -1
+ private val accessibleSlots = IntArray(14) { it }
+ private var lastNotePulseTick: Long = Long.MIN_VALUE
+
+ fun handleUseItem(stack: ItemStack, state: BlockState, level: Level, pos: BlockPos, player: Player, hit: BlockHitResult): ItemInteractionResult {
+ if (stack.isEmpty) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION
+ val slot = getHitSlot(hit, state).orElse(-1)
+ if (slot !in 0 until 14) return ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION
+ if (!isValidItem(stack)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION
+
+ val oldStack = items[slot]
+ if (!oldStack.isEmpty) {
+ if (!player.addItem(oldStack)) {
+ Containers.dropItemStack(level, pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, oldStack)
+ }
+ }
+
+ items[slot] = stack.copyWithCount(1)
+ stack.shrink(1)
+ lastInteractedSlot = slot
+ updateBlockState(level, pos)
+ markUpdated()
+
+ return ItemInteractionResult.sidedSuccess(level.isClientSide)
+ }
+
+ fun handleUseWithoutItem(state: BlockState, level: Level, pos: BlockPos, player: Player, hit: BlockHitResult): InteractionResult {
+ val slot = getHitSlot(hit, state).orElse(-1)
+ if (slot !in 0 until 14 || items[slot].isEmpty) return InteractionResult.PASS
+
+ val removed = items[slot]
+ items[slot] = ItemStack.EMPTY
+ if (!player.addItem(removed)) {
+ Containers.dropItemStack(level, pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, removed)
+ }
+
+ lastInteractedSlot = slot
+ updateBlockState(level, pos)
+
+ return InteractionResult.sidedSuccess(level.isClientSide)
+ }
+
+ private fun updateBlockState(level: Level, pos: BlockPos) {
+ val newState = TMShelfBlock.SLOT_OCCUPIED_PROPERTIES.fold(blockState) { acc, prop ->
+ val index = TMShelfBlock.SLOT_OCCUPIED_PROPERTIES.indexOf(prop)
+ acc.setValue(prop, !items[index].isEmpty)
+ }
+ level.setBlock(pos, newState, 3)
+ level.updateNeighbourForOutputSignal(pos, newState.block)
+ level.gameEvent(GameEvent.BLOCK_CHANGE, pos, GameEvent.Context.of(newState))
+ }
+
+ private fun getHitSlot(hit: BlockHitResult, state: BlockState): OptionalInt {
+ val facing = state.getValue(TMShelfBlock.FACING)
+ if (hit.direction != facing) {
+ return OptionalInt.empty()
+ }
+
+ val relative = hit.location.subtract(hit.blockPos.x.toDouble(), hit.blockPos.y.toDouble(), hit.blockPos.z.toDouble())
+
+ val (x, y) = when (facing) {
+ // Face-local X where 0 = visual left and 1 = visual right.
+ Direction.NORTH -> 1.0 - relative.x to relative.y
+ Direction.SOUTH -> relative.x to relative.y
+ Direction.WEST -> 1.0 - relative.z to relative.y
+ Direction.EAST -> relative.z to relative.y
+ else -> return OptionalInt.empty()
+ }
+
+ // Have interaction segments match the renderer areas visually
+ val leftSlotMinX = 1.0 / 16.0
+ val leftSlotMaxX = 7.0 / 16.0
+ val rightSlotMinX = 9.0 / 16.0
+ val rightSlotMaxX = 15.0 / 16.0
+ val slotMinY = 1.0 / 16.0
+ val slotMaxY = 15.0 / 16.0
+ val slotHeight = 2.0 / 16.0
+
+ val col = when {
+ x in leftSlotMinX..leftSlotMaxX -> 0
+ x in rightSlotMinX..rightSlotMaxX -> 1
+ else -> return OptionalInt.empty()
+ }
+
+ if (y < slotMinY || y > slotMaxY) {
+ return OptionalInt.empty()
+ }
+
+ val rowFromBottom = ((y - slotMinY) / slotHeight).toInt().coerceIn(0, 6)
+ val row = 6 - rowFromBottom
+
+ return OptionalInt.of(row * 2 + col)
+ }
+
+ fun isValidItem(stack: ItemStack): Boolean {
+ return stack.item is TechnicalMachineItem
+ || stack.item == CobblemonItems.UPGRADE
+ || stack.item == CobblemonItems.DUBIOUS_DISC
+ || stack.item == Items.MUSIC_DISC_13
+ || stack.item == Items.MUSIC_DISC_CAT
+ || stack.item == Items.MUSIC_DISC_BLOCKS
+ || stack.item == Items.MUSIC_DISC_CHIRP
+ || stack.item == Items.MUSIC_DISC_FAR
+ || stack.item == Items.MUSIC_DISC_MALL
+ || stack.item == Items.MUSIC_DISC_MELLOHI
+ || stack.item == Items.MUSIC_DISC_STAL
+ || stack.item == Items.MUSIC_DISC_STRAD
+ || stack.item == Items.MUSIC_DISC_WARD
+ || stack.item == Items.MUSIC_DISC_11
+ || stack.item == Items.MUSIC_DISC_WAIT
+ || stack.item == Items.MUSIC_DISC_OTHERSIDE
+ || stack.item == Items.MUSIC_DISC_5
+ || stack.item == Items.MUSIC_DISC_RELIC
+ || stack.item == Items.MUSIC_DISC_PRECIPICE
+ || stack.item == Items.MUSIC_DISC_CREATOR
+ || stack.item == Items.MUSIC_DISC_CREATOR_MUSIC_BOX
+ || stack.item == Items.MUSIC_DISC_PIGSTEP
+ }
+
+ fun onNoteBlockPulse(level: Level, noteBlockPos: BlockPos, soundEvent: SoundEvent) {
+ val gameTime = level.gameTime
+ if (lastNotePulseTick == gameTime) {
+ return
+ }
+ lastNotePulseTick = gameTime
+
+ val octaveUp = !items[1].isEmpty
+ val octaveDown = !items[12].isEmpty
+ val octaveShift = when {
+ octaveUp && !octaveDown -> 12
+ octaveDown && !octaveUp -> -12
+ else -> 0
+ }
+
+ NOTE_SLOT_TO_SEMITONE.forEach { (slot, semitone) ->
+ if (items[slot].isEmpty) return@forEach
+ val pitch = 2.0.pow((semitone + octaveShift) / 12.0).toFloat()
+ level.playSound(null, noteBlockPos, soundEvent, SoundSource.BLOCKS, 3.0f, pitch)
+ }
+ }
+
+ fun markUpdated() {
+ level?.setBlock(blockPos, blockState, 3)
+ level?.sendBlockUpdated(blockPos, blockState, blockState, 3)
+ setChanged()
+ }
+
+ override fun clearContent() {
+ var changed = false
+ for (index in items.indices) {
+ if (!items[index].isEmpty) {
+ items[index] = ItemStack.EMPTY
+ changed = true
+ }
+ }
+ if (changed) {
+ level?.let { updateBlockState(it, blockPos) }
+ markUpdated()
+ }
+ }
+
+ override fun getContainerSize(): Int = items.size
+
+ override fun isEmpty(): Boolean = items.all { it.isEmpty }
+
+ override fun getItem(slot: Int): ItemStack {
+ if (slot !in items.indices) return ItemStack.EMPTY
+ return items[slot]
+ }
+
+ override fun removeItem(slot: Int, amount: Int): ItemStack {
+ val removed = ContainerHelper.removeItem(items, slot, amount)
+ if (!removed.isEmpty) {
+ if (items[slot].isEmpty) {
+ lastInteractedSlot = slot
+ }
+ level?.let { updateBlockState(it, blockPos) }
+ markUpdated()
+ }
+ return removed
+ }
+
+ override fun removeItemNoUpdate(slot: Int): ItemStack {
+ val removed = ContainerHelper.takeItem(items, slot)
+ if (!removed.isEmpty) {
+ level?.let { updateBlockState(it, blockPos) }
+ markUpdated()
+ }
+ return removed
+ }
+
+ override fun setItem(slot: Int, stack: ItemStack) {
+ if (slot !in items.indices) return
+ if (!stack.isEmpty && !isValidItem(stack)) return
+
+ items[slot] = if (stack.isEmpty) ItemStack.EMPTY else stack.copyWithCount(1)
+ lastInteractedSlot = slot
+ level?.let { updateBlockState(it, blockPos) }
+ markUpdated()
+ }
+
+ override fun stillValid(player: Player): Boolean {
+ return level?.getBlockEntity(blockPos) === this &&
+ player.distanceToSqr(blockPos.x + 0.5, blockPos.y + 0.5, blockPos.z + 0.5) <= 64.0
+ }
+
+ override fun getMaxStackSize(): Int = 1
+
+ override fun canPlaceItem(slot: Int, stack: ItemStack): Boolean {
+ return slot in items.indices && items[slot].isEmpty && isValidItem(stack)
+ }
+
+ override fun getSlotsForFace(side: Direction): IntArray = accessibleSlots
+
+ override fun canPlaceItemThroughFace(slot: Int, stack: ItemStack, direction: Direction?): Boolean {
+ return canPlaceItem(slot, stack)
+ }
+
+ override fun canTakeItemThroughFace(slot: Int, stack: ItemStack, direction: Direction): Boolean {
+ return slot in items.indices
+ }
+
+ override fun getUpdateTag(registries: HolderLookup.Provider): CompoundTag {
+ return saveWithoutMetadata(registries)
+ }
+
+ override fun getUpdatePacket(): ClientboundBlockEntityDataPacket {
+ return ClientboundBlockEntityDataPacket.create(this)
+ }
+
+ override fun saveAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
+ super.saveAdditional(tag, registries)
+ ContainerHelper.saveAllItems(tag, items, true, registries)
+ tag.putInt("LastInteractedSlot", lastInteractedSlot)
+ }
+
+ override fun loadAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
+ super.loadAdditional(tag, registries)
+ items.clear()
+ ContainerHelper.loadAllItems(tag, items, registries)
+ lastInteractedSlot = tag.getInt("LastInteractedSlot")
+ }
+
+ companion object {
+ // C=0 chromatic semitone mapping.
+ private val NOTE_SLOT_TO_SEMITONE = linkedMapOf(
+ 0 to 5, // F
+ 2 to 4, // E
+ 4 to 3, // D#
+ 6 to 2, // D
+ 8 to 1, // C#
+ 10 to 0, // C
+ 3 to 11, // B
+ 5 to 10, // A#
+ 7 to 9, // A
+ 9 to 8, // G#
+ 11 to 7, // G
+ 13 to 6 // F#
+ )
+ }
+
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/multiblock/FossilMultiblockBuilder.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/multiblock/FossilMultiblockBuilder.kt
index bd26810891..fb96744e30 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/block/multiblock/FossilMultiblockBuilder.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/multiblock/FossilMultiblockBuilder.kt
@@ -14,7 +14,9 @@ import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.multiblock.MultiblockEntity
import com.cobblemon.mod.common.api.multiblock.builder.MultiblockStructureBuilder
import com.cobblemon.mod.common.api.multiblock.condition.BlockRelativeCondition
+import com.cobblemon.mod.common.block.MonitorBlock
import com.cobblemon.mod.common.block.RestorationTankBlock
+import com.cobblemon.mod.common.block.entity.FossilMultiblockEntity
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.blockPositionsAsList
import net.minecraft.advancements.critereon.BlockPredicate
@@ -121,6 +123,13 @@ class FossilMultiblockBuilder(val centerPos: BlockPos) : MultiblockStructureBuil
val analyzerEntity = world.getBlockEntity(fossilAnalyzerPos) as? MultiblockEntity
val tankBaseEntity = world.getBlockEntity(restorationTankPos) as? MultiblockEntity
val tankTopEntity = world.getBlockEntity(restorationTankPos.above()) as? MultiblockEntity
+ if (monitorEntity is FossilMultiblockEntity) {
+ val monitorState = world.getBlockState(monitorPos)
+ monitorEntity.dropDisk(world, monitorPos, monitorState)
+ if (monitorState.hasProperty(MonitorBlock.SCREEN)) {
+ world.setBlockAndUpdate(monitorPos, monitorState.setValue(MonitorBlock.SCREEN, MonitorBlock.MonitorScreen.OFF))
+ }
+ }
val structure = FossilMultiblockStructure(monitorPos, fossilAnalyzerPos, restorationTankPos)
structure.tankConnectorDirection = dirsToCheck.filter {
@@ -168,4 +177,4 @@ class FossilMultiblockBuilder(val centerPos: BlockPos) : MultiblockStructureBuil
//lol thanks mojang for not allowing nbt puts to be chained
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/tmmachine/TMMachineBlock.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/tmmachine/TMMachineBlock.kt
new file mode 100644
index 0000000000..ebfa1ed951
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/tmmachine/TMMachineBlock.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.tmmachine
+
+import com.cobblemon.mod.common.CobblemonBlockEntities
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.block.entity.TMMachineBlockEntity
+import com.cobblemon.mod.common.util.cobblemonResource
+import com.cobblemon.mod.common.util.playSoundServer
+import com.cobblemon.mod.common.util.toVec3d
+import com.mojang.serialization.MapCodec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import net.minecraft.core.BlockPos
+import net.minecraft.core.Direction
+import net.minecraft.server.level.ServerPlayer
+import net.minecraft.world.Containers
+import net.minecraft.world.InteractionResult
+import net.minecraft.world.entity.Entity
+import net.minecraft.world.entity.LivingEntity
+import net.minecraft.world.entity.player.Player
+import net.minecraft.world.item.context.BlockPlaceContext
+import net.minecraft.world.level.BlockGetter
+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.RenderShape
+import net.minecraft.world.level.block.SimpleWaterloggedBlock
+import net.minecraft.world.level.block.entity.BlockEntity
+import net.minecraft.world.level.block.entity.BlockEntityTicker
+import net.minecraft.world.level.block.entity.BlockEntityType
+import net.minecraft.world.level.block.state.BlockState
+import net.minecraft.world.level.block.state.StateDefinition
+import net.minecraft.world.level.block.state.properties.BlockStateProperties
+import net.minecraft.world.level.block.state.properties.BooleanProperty
+import net.minecraft.world.level.gameevent.GameEvent
+import net.minecraft.world.level.material.FluidState
+import net.minecraft.world.level.material.Fluids
+import net.minecraft.world.phys.BlockHitResult
+import net.minecraft.world.phys.shapes.CollisionContext
+import net.minecraft.world.phys.shapes.Shapes
+import net.minecraft.world.phys.shapes.VoxelShape
+import org.jetbrains.annotations.Nullable
+
+class TMMachineBlock(properties: Properties) : BaseEntityBlock(properties), SimpleWaterloggedBlock {
+
+ companion object {
+ val EMPTY: BooleanProperty = BooleanProperty.create("empty")
+ val FACING = BlockStateProperties.HORIZONTAL_FACING
+ val WATERLOGGED = BlockStateProperties.WATERLOGGED
+ val ACTIVE: BooleanProperty = BooleanProperty.create("active")
+ val OPEN: BooleanProperty = BooleanProperty.create("open")
+
+ private val SHAPE: VoxelShape = Shapes.box(0.0, 0.0, 0.0, 1.0, 0.375, 1.0)
+
+ private val SHAPE_OPEN: VoxelShape = Shapes.box(0.0, 0.0, 0.0, 1.0, 0.3125, 1.0)
+
+ private val SHAPE_OPEN_NORTH: VoxelShape = Shapes.or(
+ Shapes.box(0.0, 0.0, 0.0, 1.0, 0.3125, 1.0),
+ Shapes.box(0.0, 0.3125, 0.8125, 1.0, 0.4375, 0.9375),
+ Shapes.box(0.0, 0.4375, 0.6875, 1.0, 0.5625, 0.8125),
+ Shapes.box(0.0, 0.5625, 0.5625, 1.0, 0.6875, 0.6875),
+ Shapes.box(0.0, 0.6875, 0.4375, 1.0, 0.8125, 0.5625),
+ Shapes.box(0.0, 0.8125, 0.3125, 1.0, 0.9375, 0.4375),
+ Shapes.box(0.0, 0.9375, 0.3125, 1.0, 1.0, 0.375)
+ )
+
+ private val SHAPE_OPEN_SOUTH: VoxelShape = Shapes.or(
+ Shapes.box(0.0, 0.0, 0.0, 1.0, 0.3125, 1.0),
+ Shapes.box(0.0, 0.3125, 0.0625, 1.0, 0.4375, 0.1875),
+ Shapes.box(0.0, 0.4375, 0.1875, 1.0, 0.5625, 0.3125),
+ Shapes.box(0.0, 0.5625, 0.3125, 1.0, 0.6875, 0.4375),
+ Shapes.box(0.0, 0.6875, 0.4375, 1.0, 0.8125, 0.5625),
+ Shapes.box(0.0, 0.8125, 0.5625, 1.0, 0.9375, 0.6875),
+ Shapes.box(0.0, 0.9375, 0.625, 1.0, 1.0, 0.6875)
+ )
+
+ private val SHAPE_OPEN_EAST: VoxelShape = Shapes.or(
+ Shapes.box(0.0, 0.0, 0.0, 1.0, 0.3125, 1.0),
+ Shapes.box(0.0625, 0.3125, 0.0, 0.1875, 0.4375, 1.0),
+ Shapes.box(0.1875, 0.4375, 0.0, 0.3125, 0.5625, 1.0),
+ Shapes.box(0.3125, 0.5625, 0.0, 0.4375, 0.6875, 1.0),
+ Shapes.box(0.4375, 0.6875, 0.0, 0.5625, 0.8125, 1.0),
+ Shapes.box(0.5625, 0.8125, 0.0, 0.6875, 0.9375, 1.0),
+ Shapes.box(0.625, 0.9375, 0.0, 0.6875, 1.0, 1.0)
+ )
+
+ private val SHAPE_OPEN_WEST: VoxelShape = Shapes.or(
+ Shapes.box(0.0, 0.0, 0.0, 1.0, 0.3125, 1.0),
+ Shapes.box(0.8125, 0.3125, 0.0, 0.9375, 0.4375, 1.0),
+ Shapes.box(0.6875, 0.4375, 0.0, 0.8125, 0.5625, 1.0),
+ Shapes.box(0.5625, 0.5625, 0.0, 0.6875, 0.6875, 1.0),
+ Shapes.box(0.4375, 0.6875, 0.0, 0.5625, 0.8125, 1.0),
+ Shapes.box(0.3125, 0.8125, 0.0, 0.4375, 0.9375, 1.0),
+ Shapes.box(0.3125, 0.9375, 0.0, 0.375, 1.0, 1.0)
+ )
+
+ val CODEC: MapCodec<TMMachineBlock> = RecordCodecBuilder.mapCodec { instance ->
+ instance.group(
+ Properties.CODEC.fieldOf("properties").forGetter { it.properties }
+ ).apply(instance, ::TMMachineBlock)
+ }
+ }
+
+ init {
+ registerDefaultState(stateDefinition.any()
+ .setValue(FACING, Direction.NORTH)
+ .setValue(WATERLOGGED, false)
+ .setValue(ACTIVE, false)
+ .setValue(EMPTY, true))
+ }
+
+ override fun <T : BlockEntity> getTicker(level: Level, state: BlockState, blockEntityType: BlockEntityType<T>): BlockEntityTicker<T>? {
+ return createTicker(level, blockEntityType, CobblemonBlockEntities.TM_MACHINE)
+ }
+
+ @Nullable
+ protected fun <T : BlockEntity> createTicker(level: Level, serverType: BlockEntityType<T>, clientType: BlockEntityType<out TMMachineBlockEntity>): BlockEntityTicker<T>? {
+ return if (!level.isClientSide) createTickerHelper(serverType, clientType, TMMachineBlockEntity::serverTick)
+ else null
+ }
+
+ override fun fallOn(level: Level, state: BlockState, pos: BlockPos, entity: Entity, fallDistance: Float) {
+ if (!level.isClientSide && (entity is LivingEntity) &&
+ (entity.bbWidth * entity.bbWidth * entity.bbHeight > 0.512F) &&
+ (entity.y >= (pos.y + 0.55)) &&
+ state.hasProperty(OPEN) && state.getValue(OPEN)
+ ) {
+ level.setBlockAndUpdate(pos, state.setValue(OPEN, false))
+ level.playSoundServer(position = pos.toVec3d(), sound = CobblemonSounds.TM_MACHINE_CLOSE, volume = 1F, pitch = 1F)
+ }
+ }
+
+ override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity = TMMachineBlockEntity(pos, state)
+
+ override fun getStateForPlacement(context: BlockPlaceContext): BlockState {
+ return defaultBlockState()
+ .setValue(FACING, context.horizontalDirection.opposite)
+ .setValue(WATERLOGGED, context.level.getFluidState(context.clickedPos).type == Fluids.WATER)
+ .setValue(EMPTY, true)
+ .setValue(ACTIVE, false)
+ .setValue(OPEN, false)
+ }
+
+ override fun useWithoutItem(state: BlockState, level: Level, pos: BlockPos, player: Player, hit: BlockHitResult): InteractionResult {
+ if (!level.isClientSide) {
+ if (player.isCrouching) {
+ if (state.hasProperty(OPEN)) {
+ val isOpen = state.getValue(OPEN)
+ level.setBlockAndUpdate(pos, state.setValue(OPEN, !isOpen))
+ level.playSoundServer(
+ position = pos.toVec3d(),
+ sound = if (!isOpen) CobblemonSounds.TM_MACHINE_CLOSE else CobblemonSounds.TM_MACHINE_OPEN,
+ volume = 1F,
+ pitch = 1F
+ )
+ }
+ } else {
+ val serverPlayer = player as? ServerPlayer
+ if (serverPlayer != null) {
+ val recipeId = cobblemonResource("tm_machine")
+ val recipeOpt = level.server?.recipeManager?.byKey(recipeId)
+ recipeOpt?.ifPresent { recipe ->
+ if (!serverPlayer.recipeBook.contains(recipe)) {
+ serverPlayer.awardRecipes(listOf(recipe))
+ }
+ }
+ }
+ val blockEntity = level.getBlockEntity(pos)
+ if (blockEntity is TMMachineBlockEntity) {
+ player.openMenu(blockEntity)
+ level.playSoundServer(
+ position = pos.toVec3d(),
+ sound = CobblemonSounds.TM_MACHINE_ON,
+ volume = 0.5F,
+ pitch = 1F
+ )
+ level.gameEvent(player, GameEvent.BLOCK_OPEN, pos)
+ }
+ }
+ }
+ return InteractionResult.sidedSuccess(level.isClientSide)
+ }
+
+ override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
+ builder.add(FACING, WATERLOGGED, EMPTY, ACTIVE, OPEN)
+ }
+
+ override fun codec(): MapCodec<TMMachineBlock> = CODEC
+
+ override fun getRenderShape(state: BlockState): RenderShape = RenderShape.MODEL
+
+ override fun getFluidState(state: BlockState): FluidState =
+ if (state.getValue(WATERLOGGED)) Fluids.WATER.defaultFluidState() else super.getFluidState(state)
+
+ override fun getCollisionShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape {
+ return if (state.getValue(OPEN)) {
+ when (state.getValue(FACING)) {
+ Direction.NORTH -> SHAPE_OPEN_NORTH
+ Direction.SOUTH -> SHAPE_OPEN_SOUTH
+ Direction.WEST -> SHAPE_OPEN_WEST
+ Direction.EAST -> SHAPE_OPEN_EAST
+ else -> Shapes.empty()
+ }
+ } else SHAPE
+ }
+
+ override fun getShape(state: BlockState, getter: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = if (state.getValue(OPEN)) SHAPE_OPEN else SHAPE
+
+ override fun onRemove(state: BlockState, level: Level, pos: BlockPos, newState: BlockState, movedByPiston: Boolean) {
+ Containers.dropContentsOnDestroy(state, newState, level, pos)
+ super.onRemove(state, level, pos, newState, movedByPiston)
+ }
+
+ override fun isSignalSource(state: BlockState): Boolean = false
+
+ override fun getSignal(state: BlockState, level: BlockGetter, pos: BlockPos, direction: Direction): Int = 0
+
+ override fun hasAnalogOutputSignal(state: BlockState): Boolean = true
+
+ /*override fun getAnalogOutputSignal(state: BlockState, level: Level, pos: BlockPos): Int {
+ val be = level.getBlockEntity(pos) as? TMMachineBlockEntity ?: return 0
+
+ // Have comparator output act like a progress bar when burning because that might be super dope to see without going into the menu
+ val scaled = (be.burnProgress.toFloat() / TMMachineBlockEntity.TOTAL_PROCESS_TIME.toFloat() * 15f).toInt()
+ return scaled.coerceIn(0, 15)
+ }*/
+
+ override fun getAnalogOutputSignal(state: BlockState, level: Level, pos: BlockPos): Int {
+ val be = level.getBlockEntity(pos) as? TMMachineBlockEntity ?: return 0
+
+ // Have comparator output act like a progress bar when burning because that might be super dope to see without going into the menu
+ if (be.burnActive) {
+ val scaled = (be.burnProgress.toFloat() / TMMachineBlockEntity.TOTAL_PROCESS_TIME.toFloat() * 15F).toInt()
+ return scaled.coerceIn(0, 15)
+ }
+
+ // if batch mode is off but it is ready to craft then show signal of 15
+ if (!be.repeatProcess) {
+ if (be.isReadyToCraft()) return 15
+ else return 0
+ }
+
+ // When not burning but idling in batch mode maybe show some cool states? Could be nice
+ return when {
+ be.isOutputBlocked() -> 8
+ else -> 4
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/block/tmmachine/TMMachineMenu.kt b/common/src/main/kotlin/com/cobblemon/mod/common/block/tmmachine/TMMachineMenu.kt
new file mode 100644
index 0000000000..ee86608259
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/block/tmmachine/TMMachineMenu.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.tmmachine
+
+import com.cobblemon.mod.common.CobblemonItems
+import com.cobblemon.mod.common.CobblemonMenuType
+import com.cobblemon.mod.common.api.moves.Moves
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.block.entity.TMMachineBlockEntity
+import com.cobblemon.mod.common.util.itemRegistry
+import net.minecraft.server.level.ServerLevel
+import net.minecraft.util.Mth
+import net.minecraft.world.Container
+import net.minecraft.world.SimpleContainer
+import net.minecraft.world.entity.player.Inventory
+import net.minecraft.world.entity.player.Player
+import net.minecraft.world.inventory.*
+import net.minecraft.world.item.ItemStack
+
+class TMMachineMenu(menuType: MenuType<*>, syncId: Int) : AbstractContainerMenu(menuType, syncId), ContainerListener {
+ companion object {
+ const val RESULT_SLOT = 0
+ const val BLANK_TM_SLOT = 1
+ val INGREDIENT_SLOTS = 2..4
+ }
+
+ var playerInventory: Inventory? = null
+ var tmMachineEntity: TMMachineBlockEntity? = null
+ var inventory: Container? = null
+ var containerData: ContainerData? = null
+
+ constructor(syncId: Int, playerInventory: Inventory) : this(CobblemonMenuType.TM_MACHINE, syncId) {
+ this.playerInventory = playerInventory
+ this.inventory = SimpleContainer(5)
+ this.containerData = SimpleContainerData(6)
+ this.tmMachineEntity = (inventory as? TMMachineBlockEntity.TMMachineBlockInventory)?.blockEntity
+
+ this.addDataSlots(containerData!!)
+ inventory?.startOpen(playerInventory.player)
+
+ val startX = 15
+ val startY = 144
+ val slotWidth = 18
+ val slotHeight = 18
+
+ for (row in 0..2) {
+ for (col in 0..8) {
+ this.addSlot(Slot(playerInventory, 9 + (row * 9) + col, startX + (slotWidth * col), startY + (slotHeight * row)))
+ }
+ }
+ for (col in 0..8) {
+ this.addSlot(Slot(playerInventory, col, startX + (slotWidth * col), startY + 58))
+ }
+
+ val container = inventory
+ container?.let {
+ this.addSlot(TMResultSlot(container, RESULT_SLOT, startX + 37, startY - 28)) // Output slot
+ this.addSlot(BlankTMSlot(container, BLANK_TM_SLOT, startX + 90, startY - 28)) // Blank TM
+ for (index in INGREDIENT_SLOTS) {
+ this.addSlot(Slot(container, index, startX + 114 + (18 * (index - INGREDIENT_SLOTS.first)), startY - 28))
+ }
+ }
+ this.addSlotListener(this)
+ }
+
+ constructor(syncId: Int, playerInventory: Inventory, inventory: Container, containerData: ContainerData) : this(CobblemonMenuType.TM_MACHINE, syncId) {
+ this.playerInventory = playerInventory
+ this.inventory = inventory
+ this.containerData = containerData
+ this.tmMachineEntity = (inventory as? TMMachineBlockEntity.TMMachineBlockInventory)?.blockEntity
+
+ this.addDataSlots(containerData)
+ inventory.startOpen(playerInventory.player)
+
+ val startX = 15
+ val startY = 144
+ val slotWidth = 18
+ val slotHeight = 18
+
+ for (row in 0..2) {
+ for (col in 0..8) {
+ this.addSlot(Slot(playerInventory, 9 + (row * 9) + col, startX + (slotWidth * col), startY + (slotHeight * row)))
+ }
+ }
+ for (col in 0..8) {
+ this.addSlot(Slot(playerInventory, col, startX + (slotWidth * col), startY + 58))
+ }
+
+ val container = inventory
+ container.let {
+ this.addSlot(TMResultSlot(container, RESULT_SLOT, startX + 37, startY - 28)) // Output slot
+ this.addSlot(BlankTMSlot(container, BLANK_TM_SLOT, startX + 90, startY - 28)) // Blank TM
+ for (index in INGREDIENT_SLOTS) {
+ this.addSlot(Slot(container, index, startX + 114 + (18 * (index - INGREDIENT_SLOTS.first)), startY - 28))
+ }
+ }
+ this.addSlotListener(this)
+ }
+
+ override fun quickMoveStack(player: Player, index: Int): ItemStack {
+ var itemStack = ItemStack.EMPTY
+ val slot = this.slots[index]
+ val maxPlayerInventoryIndex = Inventory.INVENTORY_SIZE - 1
+
+ if (slot.hasItem()) {
+ val movedStack = slot.item
+ itemStack = movedStack.copy()
+
+ // If in container slots, move to player inventory
+ // 36: Output slot, 37: Blank TM slot, 38-40: Ingredient slots
+ if (index in 36..40) {
+ if (!this.moveItemStackTo(movedStack, 0, maxPlayerInventoryIndex, false)) return ItemStack.EMPTY
+ }
+ // If in player inventory, move to container slots. 'endIndex' is non-inclusive
+ else if (index <= maxPlayerInventoryIndex) {
+ if (!this.moveItemStackTo(movedStack, 37, 40 + 1, false)) return ItemStack.EMPTY
+ }
+
+ if (movedStack.isEmpty) {
+ slot.setByPlayer(ItemStack.EMPTY)
+ } else {
+ slot.setChanged()
+ }
+ }
+ return itemStack
+ }
+
+ override fun stillValid(player: Player): Boolean {
+ return player.let { inventory?.stillValid(it) } ?: false
+ }
+
+ fun getBurnProgressValue(): Int = this.containerData?.get(TMMachineBlockEntity.BURN_PROGRESS_INDEX) ?: 0
+
+ fun getBurnProgressRatio(): Float {
+ val amount = getBurnProgressValue()
+ val max = TMMachineBlockEntity.BURN_TOTAL_TIME
+ return if (amount != 0) {
+ Mth.clamp((amount.toFloat() / max.toFloat()), 0.0F, 1.0F)
+ } else {
+ 0.0F
+ }
+ }
+
+ fun getPostCraftTicks(): Int {
+ return if (getBurnProgressValue() >= TMMachineBlockEntity.BURN_TOTAL_TIME) {
+ getBurnProgressValue() - TMMachineBlockEntity.BURN_TOTAL_TIME
+ } else 0
+ }
+
+ override fun removed(player: Player) {
+ super.removed(player)
+ tmMachineEntity?.setChanged()
+ }
+
+ class TMResultSlot(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
+ override fun mayPlace(stack: ItemStack): Boolean = false
+ }
+
+ class BlankTMSlot(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
+ override fun mayPlace(stack: ItemStack): Boolean = super.mayPlace(stack) && (stack.item == CobblemonItems.BLANK_TM)
+ }
+
+ override fun slotChanged(containerToSend: AbstractContainerMenu, dataSlotIndex: Int, stack: ItemStack) {
+ val level = playerInventory?.player?.level()
+
+ // If ingredient slots have been modified and level is server side and process is active
+ // 36: Output slot, 37: Blank TM slot, 38-40: Ingredient slots
+ if ((dataSlotIndex in 37 .. 40) && (level?.isClientSide == false) && (containerData?.get(TMMachineBlockEntity.BURN_ACTIVE_INDEX) == 1)) {
+ // Stop process if there are not enough ingredients after slots have been modified
+ var validCost = !(inventory?.getItem(BLANK_TM_SLOT)?.isEmpty ?: true)
+
+ if (validCost) {
+ Moves.getByName(tmMachineEntity?.activeMove ?: "")?.let { moveTemplate ->
+ TechnicalMachines.moveToTM[moveTemplate]?.let { tm ->
+ val recipes = tm.getClampedRecipe() ?: emptyList()
+ for ((index, recipe) in recipes.withIndex()) {
+ val slot = INGREDIENT_SLOTS.first + index
+ val provided = inventory?.getItem(slot) ?: ItemStack.EMPTY
+
+ if (!recipe.ingredient.test(provided) || provided.count < recipe.count) {
+ validCost = false
+ break
+ }
+ }
+ }
+ }
+ }
+
+ if (!validCost) {
+ containerData?.set(TMMachineBlockEntity.BURN_ACTIVE_INDEX, 0)
+ containerData?.set(TMMachineBlockEntity.BURN_PROGRESS_INDEX, 0)
+ }
+ }
+
+ tmMachineEntity?.setChanged()
+ inventory?.setChanged()
+ playerInventory?.setChanged()
+ broadcastChanges()
+ }
+
+ override fun dataChanged(containerMenu: AbstractContainerMenu, dataSlotIndex: Int, value: Int) {
+ broadcastChanges()
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/CobblemonClient.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/CobblemonClient.kt
index c995c97728..36fadaab40 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/CobblemonClient.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/CobblemonClient.kt
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.api.berry.Berries
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker
import com.cobblemon.mod.common.api.storage.player.client.ClientGeneralPlayerData
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
+import com.cobblemon.mod.common.api.storage.player.client.ClientTMMoveManager
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.block.entity.TintBlockEntity
import com.cobblemon.mod.common.client.battle.ClientBattle
@@ -27,6 +28,7 @@ import com.cobblemon.mod.common.client.gui.PartyOverlayDataControl
import com.cobblemon.mod.common.client.gui.RideControlsOverlay
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.gui.cookingpot.CookingPotScreen
+import com.cobblemon.mod.common.client.gui.tmmachine.TMMachineScreen
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
@@ -40,7 +42,9 @@ 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.PokeSnackBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.RestorationTankRenderer
+import com.cobblemon.mod.common.client.render.block.TMShelfBlockEntityRenderer
import com.cobblemon.mod.common.client.render.boat.CobblemonBoatRenderer
+import com.cobblemon.mod.common.client.render.color.TechnicalMachineItemColorProvider
import com.cobblemon.mod.common.client.render.color.AprijuiceItemColorProvider
import com.cobblemon.mod.common.client.render.color.PokeBaitItemColorProvider
import com.cobblemon.mod.common.client.render.color.PokeSnackItemColorProvider
@@ -69,6 +73,7 @@ import com.cobblemon.mod.common.client.tooltips.FishingRodTooltipGenerator
import com.cobblemon.mod.common.client.tooltips.PokePuffTooltipGenerator
import com.cobblemon.mod.common.client.tooltips.RecipeSeasoningAbsorptionTooltipGenerator
import com.cobblemon.mod.common.client.tooltips.SeasoningTooltipGenerator
+import com.cobblemon.mod.common.client.tooltips.TechnicalMachineTooltipGenerator
import com.cobblemon.mod.common.client.tooltips.TooltipManager
import com.cobblemon.mod.common.client.trade.ClientTrade
import com.cobblemon.mod.common.entity.boat.CobblemonBoatType
@@ -89,6 +94,7 @@ import net.minecraft.client.renderer.blockentity.HangingSignRenderer
import net.minecraft.client.renderer.blockentity.SignRenderer
import net.minecraft.client.renderer.entity.EntityRenderer
import net.minecraft.client.renderer.entity.LivingEntityRenderer
+import net.minecraft.client.renderer.entity.layers.RenderLayer
import net.minecraft.client.resources.PlayerSkin
import net.minecraft.server.packs.resources.ResourceManager
import net.minecraft.world.InteractionHand
@@ -103,6 +109,7 @@ object CobblemonClient {
var battle: ClientBattle? = null
var clientPlayerData = ClientGeneralPlayerData()
var clientPokedexData = ClientPokedexManager(mutableMapOf())
+ var clientTMMoveData = ClientTMMoveManager(mutableSetOf())
/** If true then we won't bother them anymore about choosing a starter even if it's a thing they can do. */
var checkedStarterScreen = false
@@ -119,6 +126,7 @@ object CobblemonClient {
requests = ClientPlayerActionRequests()
teamData = ClientPlayerTeamData()
clientPokedexData = ClientPokedexManager(mutableMapOf())
+ clientTMMoveData = ClientTMMoveManager(mutableSetOf())
storage.onLogin()
// CobblemonDataProvider.canReload = false
}
@@ -206,6 +214,7 @@ object CobblemonClient {
TooltipManager.registerTooltipGenerator(SeasoningTooltipGenerator)
TooltipManager.registerTooltipGenerator(FishingRodTooltipGenerator)
TooltipManager.registerTooltipGenerator(AprijuiceTooltipGenerator)
+ TooltipManager.registerTooltipGenerator(TechnicalMachineTooltipGenerator)
TooltipManager.registerTooltipGenerator(PokePuffTooltipGenerator)
}
@@ -287,6 +296,7 @@ object CobblemonClient {
CobblemonBlocks.POTTED_PEP_UP_FLOWER,
CobblemonBlocks.REVIVAL_HERB,
*CobblemonBlocks.berries().values.toTypedArray(),
+ CobblemonBlocks.POTTED_PEP_UP_FLOWER,
CobblemonBlocks.GALARICA_NUT_BUSH,
CobblemonBlocks.RESTORATION_TANK,
CobblemonBlocks.SMALL_BUDDING_TUMBLESTONE,
@@ -297,6 +307,24 @@ object CobblemonClient {
CobblemonBlocks.MEDIUM_BUDDING_BLACK_TUMBLESTONE,
CobblemonBlocks.LARGE_BUDDING_BLACK_TUMBLESTONE,
CobblemonBlocks.BLACK_TUMBLESTONE_CLUSTER,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_NORMAL,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_FIRE,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_WATER,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_ELECTRIC,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_GRASS,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_ICE,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_FIGHTING,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_POISON,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_GROUND,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_FLYING,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_PSYCHIC,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_BUG,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_ROCK,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_GHOST,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_DRAGON,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_DARK,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_STEEL,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_FAIRY,
CobblemonBlocks.SMALL_BUDDING_SKY_TUMBLESTONE,
CobblemonBlocks.MEDIUM_BUDDING_SKY_TUMBLESTONE,
CobblemonBlocks.LARGE_BUDDING_SKY_TUMBLESTONE,
@@ -362,7 +390,8 @@ object CobblemonClient {
CobblemonBlocks.X_DEFENSE,
CobblemonBlocks.X_SP_ATK,
CobblemonBlocks.X_SP_DEF,
- CobblemonBlocks.X_SPEED
+ CobblemonBlocks.X_SPEED,
+ CobblemonBlocks.TM_MACHINE
)
this.createBoatModelLayers()
@@ -390,6 +419,7 @@ object CobblemonClient {
private fun registerMenuScreens() {
MenuScreens.register(CobblemonMenuType.COOKING_POT, ::CookingPotScreen)
+ MenuScreens.register(CobblemonMenuType.TM_MACHINE, ::TMMachineScreen)
}
private fun registerBlockEntityRenderers() {
@@ -413,6 +443,7 @@ object CobblemonClient {
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.LECTERN, ::LecternBlockEntityRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.CAMPFIRE, ::CampfireBlockEntityRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.POKE_SNACK, ::PokeSnackBlockEntityRenderer)
+ this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.TM_SHELF, ::TMShelfBlockEntityRenderer)
}
private fun registerEntityRenderers() {
@@ -421,14 +452,14 @@ object CobblemonClient {
LOGGER.info("Registering PokéBall renderer")
this.implementation.registerEntityRenderer(CobblemonEntities.EMPTY_POKEBALL, ::PokeBallRenderer)
LOGGER.info("Registering Boat renderer")
- this.implementation.registerEntityRenderer(CobblemonEntities.BOAT) { ctx -> CobblemonBoatRenderer(ctx, false) }
- LOGGER.info("Registering Boat with Chest renderer")
- this.implementation.registerEntityRenderer(CobblemonEntities.CHEST_BOAT) { ctx ->
+ this.implementation.registerEntityRenderer(CobblemonEntities.BOAT) { ctx ->
CobblemonBoatRenderer(
ctx,
true
)
}
+ LOGGER.info("Registering Boat with Chest renderer")
+ this.implementation.registerEntityRenderer(CobblemonEntities.CHEST_BOAT) { ctx -> CobblemonBoatRenderer(ctx, true) }
LOGGER.info("Registering Generic Bedrock renderer")
this.implementation.registerEntityRenderer(CobblemonEntities.GENERIC_BEDROCK_ENTITY, ::GenericBedrockRenderer)
LOGGER.info("Registering Generic Bedrock Entity renderer")
@@ -445,6 +476,7 @@ object CobblemonClient {
implementation.registerItemColors(PokeBaitItemColorProvider, CobblemonItems.POKE_BAIT)
implementation.registerItemColors(PonigiriItemColorProvider, CobblemonItems.PONIGIRI)
implementation.registerItemColors(SinisterTeaItemColorProvider, CobblemonItems.SINISTER_TEA)
+ implementation.registerItemColors(TechnicalMachineItemColorProvider, CobblemonItems.TECHNICAL_MACHINE)
}
fun reloadCodedAssets(resourceManager: ResourceManager) {
@@ -481,4 +513,4 @@ object CobblemonClient {
)
}
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/PartyOverlay.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/PartyOverlay.kt
index a2dd8bbef0..15f7e9ef0f 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/PartyOverlay.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/PartyOverlay.kt
@@ -93,7 +93,7 @@ class PartyOverlay : Gui(Minecraft.getInstance()) {
val starterToast = CobblemonToast(
Mth.createInsecureUUID(),
- CobblemonItems.POKE_BALL.defaultInstance,
+ listOf(CobblemonItems.POKE_BALL.defaultInstance),
lang("ui.starter.choose_starter_title", SummaryBinding.boundKey().displayName).red(),
lang("ui.starter.choose_starter_description", SummaryBinding.boundKey().displayName).darkGray(),
AdvancementToast.BACKGROUND_SPRITE,
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/interact/moveselect/MoveSlotButton.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/interact/moveselect/MoveSlotButton.kt
index b91d59265e..cbead029d8 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/interact/moveselect/MoveSlotButton.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/interact/moveselect/MoveSlotButton.kt
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.client.gui.interact.moveselect
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.moves.MoveTemplate
-import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.gold
import com.cobblemon.mod.common.api.text.red
@@ -21,6 +20,7 @@ import com.cobblemon.mod.common.client.gui.MoveCategoryIcon
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
+import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.toRGB
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
@@ -31,96 +31,92 @@ import net.minecraft.util.Mth
class MoveSlotButton(
x: Int, y: Int,
- val move: MoveTemplate,
- val pp: Int,
- val ppMax: Int,
- val enabled: Boolean = true,
+ var move: MoveTemplate? = null,
+ var pp: Int = 0,
+ var ppMax: Int = 0,
+ var ppAsFraction: Boolean = true,
+ var enabled: Boolean = true,
onPress: OnPress
-) : Button(x, y, WIDTH, HEIGHT, Component.literal("Move"), onPress, CreateNarration { "".text() }), CobblemonRenderable {
+) : Button(x, y, WIDTH, HEIGHT, move?.name?.text() ?: lang("ui.moves"), onPress, CreateNarration { move?.name?.text() ?: lang("ui.moves") }), CobblemonRenderable {
companion object {
private val moveResource = cobblemonResource("textures/gui/summary/summary_move.png")
+ private val moveDisabledResource = cobblemonResource("textures/gui/summary/summary_move_disabled.png")
private val moveOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay.png")
const val WIDTH = 108
const val HEIGHT = 22
}
- override fun renderWidget(context: GuiGraphics, pMouseX: Int, pMouseY: Int, pPartialTicks: Float) {
- isHovered = pMouseX >= x && pMouseY >= y && pMouseX < x + width && pMouseY < y + height && enabled
-
- val moveTemplate = Moves.getByNameOrDummy(move.name)
- val rgb = moveTemplate.elementalType.hue.toRGB()
+ override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, pPartialTicks: Float) {
+ move?.let {
+ isHovered = isMouseOver(mouseX.toDouble(), mouseY.toDouble()) && enabled
+ val rgb = it.elementalType.hue.toRGB()
+ val alpha = if (enabled) 1.0F else 0.7F
+ val matrices = context.pose()
+
+ blitk(
+ matrixStack = matrices,
+ texture = if (enabled) moveResource else moveDisabledResource,
+ x = x,
+ y = y,
+ width = WIDTH,
+ height = HEIGHT,
+ vOffset = if (isHovered) HEIGHT else 0,
+ textureHeight = HEIGHT * 2,
+ red = rgb.first,
+ green = rgb.second,
+ blue = rgb.third
+ )
- val alpha = if (enabled) 1.0 else 0.5
+ blitk(
+ matrixStack = matrices,
+ texture = moveOverlayResource,
+ x = x,
+ y = y,
+ width = WIDTH,
+ height = HEIGHT
+ )
- val matrices = context.pose()
- blitk(
- matrixStack = matrices,
- texture = moveResource,
- x = x,
- y = y,
- width = WIDTH,
- height = HEIGHT,
- vOffset = if (isHovered) HEIGHT else 0,
- textureHeight = HEIGHT * 2,
- red = rgb.first,
- green = rgb.second,
- blue = rgb.third,
- alpha = alpha
- )
+ if (pp != -1 && ppMax != -1) {
+ var movePPText = lang("ui.moves.pp", pp).bold()
+ if (ppAsFraction) {
+ movePPText = Component.literal("$pp/$ppMax").bold()
+
+ if (pp <= Mth.floor(ppMax / 2F)) {
+ movePPText = if (pp == 0) movePPText.red() else movePPText.gold()
+ }
+ }
+
+ drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = movePPText,
+ x = x + 93,
+ y = y + 13,
+ centered = true,
+ opacity = alpha
+ )
+ }
- blitk(
- matrixStack = matrices,
- texture = moveOverlayResource,
- x = x,
- y = y,
- width = WIDTH,
- height = HEIGHT,
- alpha = alpha
- )
+ // Type Icon
+ TypeIcon(x = x + 2, y = y + 2, type = it.elementalType, opacity = alpha).render(context)
- if (pp != -1 && ppMax != -1) {
- var movePPText = Component.literal("$pp/$ppMax").bold()
-
- if (pp <= Mth.floor(ppMax / 2F)) {
- movePPText = if (pp == 0) movePPText.red() else movePPText.gold()
- }
+ // Move Category
+ MoveCategoryIcon(x = x + 66, y = y + 13.5, category = it.damageCategory, opacity = alpha).render(context)
+ // Move Name
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
- text = movePPText,
- x = x + 93,
- y = y + 13,
- centered = true
+ text = it.displayName.bold(),
+ x = x + 28,
+ y = y + 2,
+ shadow = true,
+ opacity = alpha
)
}
-
- // Type Icon
- TypeIcon(
- x = x + 2,
- y = y + 2,
- type = moveTemplate.elementalType
- ).render(context)
-
- // Move Category
- MoveCategoryIcon(
- x = x + 66,
- y = y + 13.5,
- category = move.damageCategory
- ).render(context)
-
- // Move Name
- drawScaledText(
- context = context,
- font = CobblemonResources.DEFAULT_LARGE,
- text = move.displayName.bold(),
- x = x + 28,
- y = y + 2,
- shadow = true
- )
}
override fun playDownSound(soundManager: SoundManager) {}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/party/PartyTutorialToasts.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/party/PartyTutorialToasts.kt
index 7a70fb5f2a..01ef4c4ee3 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/party/PartyTutorialToasts.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/party/PartyTutorialToasts.kt
@@ -45,7 +45,7 @@ object PartyTutorialToasts {
private val toast = CobblemonToast(
Mth.createInsecureUUID(),
- CobblemonItems.POKEDEX_RED.defaultInstance,
+ listOf(CobblemonItems.POKEDEX_RED.defaultInstance),
lang("ui.party.party_select_title").red(),
lang(
"ui.party.party_select_description",
@@ -96,4 +96,4 @@ object PartyTutorialToasts {
private fun hideToast() {
if (toastAttached) toast.nextVisibility = Toast.Visibility.HIDE
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/PokedexGUI.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/PokedexGUI.kt
index edd6162449..31da2fae81 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/PokedexGUI.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/PokedexGUI.kt
@@ -22,6 +22,8 @@ import com.cobblemon.mod.common.api.pokedex.entry.PokedexForm
import com.cobblemon.mod.common.api.pokedex.filter.EntryFilter
import com.cobblemon.mod.common.api.pokedex.filter.SearchByType
import com.cobblemon.mod.common.api.pokedex.filter.SearchFilter
+import com.cobblemon.mod.common.api.pokedex.filter.SecondaryPokedexFilter
+import com.cobblemon.mod.common.api.pokedex.filter.SecondaryPokedexFilterType
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
import com.cobblemon.mod.common.api.text.bold
@@ -39,6 +41,7 @@ import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_ABILI
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_DESCRIPTION
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_DROPS
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_ICON_SIZE
+import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_MOVES
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_STATS
import com.cobblemon.mod.common.client.gui.pokedex.widgets.*
@@ -83,12 +86,14 @@ class PokedexGUI private constructor(
private val arrowDownIcon = cobblemonResource("textures/gui/pokedex/arrow_down.png")
private val tabSelectArrow = cobblemonResource("textures/gui/pokedex/select_arrow.png")
+ private val secondaryFilterIcon = cobblemonResource("textures/gui/pokedex/filter_icon_button.png")
private val tabIcons = arrayOf(
cobblemonResource("textures/gui/pokedex/tab_info.png"),
cobblemonResource("textures/gui/pokedex/tab_abilities.png"),
cobblemonResource("textures/gui/pokedex/tab_size.png"),
cobblemonResource("textures/gui/pokedex/tab_stats.png"),
- cobblemonResource("textures/gui/pokedex/tab_drops.png")
+ cobblemonResource("textures/gui/pokedex/tab_drops.png"),
+ cobblemonResource("textures/gui/pokedex/tab_moves.png")
)
/**
@@ -127,11 +132,13 @@ class PokedexGUI private constructor(
private lateinit var regionSelectWidgetUp: ScaledButton
private lateinit var regionSelectWidgetDown: ScaledButton
private lateinit var searchByTypeButton: ScaledButton
+ private lateinit var secondaryFilterButton: ScaledButton
private lateinit var scrollScreen: EntriesScrollingWidget
private lateinit var pokemonInfoWidget: PokemonInfoWidget
private lateinit var searchWidget: SearchWidget
private var selectedSearchByType: SearchByType = SearchByType.SPECIES
+ private var selectedSecondaryFilter: SecondaryPokedexFilterType = SecondaryPokedexFilterType.ALL
private val tabButtons: MutableList<ScaledButton> = mutableListOf()
lateinit var tabInfoElement: GuiEventListener
@@ -163,7 +170,7 @@ class PokedexGUI private constructor(
displaytabInfoElement(tabInfoIndex, false)
if (::searchWidget.isInitialized) removeWidget(searchWidget)
- searchWidget = SearchWidget(x + 26, y + 28, 128, HEADER_BAR_HEIGHT, update = ::updateFilters)
+ searchWidget = SearchWidget(x + 26, y + 28, 120, HEADER_BAR_HEIGHT, update = ::updateFilters)
addRenderableWidget(searchWidget)
if (::regionSelectWidgetUp.isInitialized) removeWidget(regionSelectWidgetUp)
@@ -208,6 +215,23 @@ class PokedexGUI private constructor(
)
addRenderableWidget(searchByTypeButton)
+ if (::secondaryFilterButton.isInitialized) removeWidget(secondaryFilterButton)
+ secondaryFilterButton = ScaledButton(
+ buttonX = (x + 146.5).toFloat(),
+ buttonY = (y + 29.5).toFloat(),
+ buttonWidth = 16,
+ buttonHeight = 16,
+ scale = SCALE,
+ resource = secondaryFilterIcon,
+ clickAction = {
+ val filters = SecondaryPokedexFilterType.entries.toList()
+ val selectedIndex = filters.indexOf(selectedSecondaryFilter)
+ selectedSecondaryFilter = filters[if (selectedIndex == filters.lastIndex) 0 else (selectedIndex + 1)]
+ updateFilters()
+ }
+ )
+ addRenderableWidget(secondaryFilterButton)
+
updateFilters(true)
}
@@ -309,8 +333,7 @@ class PokedexGUI private constructor(
blitk(
matrixStack = matrices,
texture = tabSelectArrow,
- x = (x + 198 + (25 * tabInfoIndex)) / SCALE,
- // (x + 191.5 + (22 * tabInfoIndex)) / SCALE for 6 tabs
+ x = (x + 191.5F + (22F * tabInfoIndex)) / SCALE,
y = (y + 177) / SCALE,
width = 12,
height = 6,
@@ -332,6 +355,18 @@ class PokedexGUI private constructor(
-14
)
}
+
+ if (secondaryFilterButton.isButtonHovered(mouseX, mouseY)) {
+ val filterText = lang("ui.pokedex.filter.${selectedSecondaryFilter.name.lowercase()}").bold()
+ renderTooltip(
+ context,
+ filterText,
+ mouseX,
+ mouseY,
+ delta,
+ -14
+ )
+ }
}
override fun onClose() {
@@ -344,6 +379,7 @@ class PokedexGUI private constructor(
val canDisplayEntry = true //selectedForm?.unlockForms
if (::pokemonInfoWidget.isInitialized
+ && !pokemonInfoWidget.isSuppressed
&& pokemonInfoWidget.isWithinPortraitSpace(mouseX, mouseY)
&& canDisplayEntry == true
) {
@@ -375,7 +411,7 @@ class PokedexGUI private constructor(
}
override fun tick() {
- if (::pokemonInfoWidget.isInitialized) pokemonInfoWidget.tick()
+ if (::pokemonInfoWidget.isInitialized && !pokemonInfoWidget.isSuppressed) pokemonInfoWidget.tick()
}
fun updatePokedexRegion(nextIndex: Boolean) {
@@ -441,6 +477,7 @@ class PokedexGUI private constructor(
fun getFilters(): Collection<EntryFilter> {
val filters: MutableList<EntryFilter> = mutableListOf()
+ filters.add(SecondaryPokedexFilter(CobblemonClient.clientPokedexData, selectedSecondaryFilter))
filters.add(SearchFilter(CobblemonClient.clientPokedexData, searchWidget.value, selectedSearchByType))
return filters
@@ -462,7 +499,7 @@ class PokedexGUI private constructor(
for (i in tabIcons.indices) {
tabButtons.add(ScaledButton(
- x + 197F + (i * 25F), // x + 190.5F + (i * 22F) for 6 tabs
+ x + 190.5F + (i * 22F),
y + 181.5F,
TAB_ICON_SIZE,
TAB_ICON_SIZE,
@@ -497,6 +534,9 @@ class PokedexGUI private constructor(
}
tabInfoIndex = tabIndex
+ if (::pokemonInfoWidget.isInitialized) {
+ pokemonInfoWidget.isSuppressed = tabInfoIndex == TAB_MOVES
+ }
if (::tabInfoElement.isInitialized) removeWidget(tabInfoElement)
val x = (width - BASE_WIDTH) / 2
@@ -520,11 +560,19 @@ class PokedexGUI private constructor(
TAB_DROPS -> {
tabInfoElement = DropsScrollingWidget(x + 189, y + 135)
}
+ TAB_MOVES -> {
+ tabInfoElement = MovesLearnsetWidget(x + 180, y + 28)
+ }
}
val element = tabInfoElement
if (element is Renderable && element is NarratableEntry) {
addRenderableWidget(element)
}
+ // Keep tab icons above the moves overlay.
+ if (tabButtons.isNotEmpty()) {
+ tabButtons.forEach { removeWidget(it) }
+ tabButtons.forEach { addRenderableWidget(it) }
+ }
if (update) updateTabInfoElement()
}
@@ -578,6 +626,17 @@ class PokedexGUI private constructor(
(tabInfoElement as DropsScrollingWidget).dropTable = form.drops
(tabInfoElement as DropsScrollingWidget).setEntries()
}
+ TAB_MOVES -> {
+ val availableForms = selectedEntry?.let { CobblemonClient.clientPokedexData.getEncounteredForms(it) } ?: emptyList()
+ val activeForm = this.selectedForm ?: availableForms.firstOrNull()
+ val fallbackForm = activeForm ?: PokedexForm().apply { displayForm = form.name }
+ (tabInfoElement as MovesLearnsetWidget).setLearnset(
+ species = species,
+ form = form,
+ availableForms = availableForms,
+ selectedForm = fallbackForm
+ ) { next -> cycleSelectedForm(next) }
+ }
// TAB_MOVES -> {
// form.moves.getLevelUpMovesUpTo(100)
// }
@@ -600,6 +659,29 @@ class PokedexGUI private constructor(
displaytabInfoElement(tabInfoIndex)
}
+ fun cycleSelectedForm(next: Boolean) {
+ val entry = selectedEntry ?: return
+ val forms = CobblemonClient.clientPokedexData.getEncounteredForms(entry)
+ if (forms.isEmpty()) return
+
+ val current = selectedForm ?: forms.first()
+ val currentIndex = forms.indexOfFirst { it.displayForm.equals(current.displayForm, ignoreCase = true) }
+ .let { if (it == -1) 0 else it }
+
+ val nextIndex = if (next) {
+ (currentIndex + 1) % forms.size
+ } else {
+ (currentIndex - 1 + forms.size) % forms.size
+ }
+
+ val newForm = forms[nextIndex]
+ selectedForm = newForm
+ if (::pokemonInfoWidget.isInitialized) {
+ pokemonInfoWidget.setSelectedForm(newForm)
+ }
+ updateTabInfoElement()
+ }
+
fun canSelectTab(tabIndex: Int): Boolean {
val selectedForm = this.selectedForm ?: return false
val selectedEntry = this.selectedEntry ?: return false
@@ -621,4 +703,4 @@ class PokedexGUI private constructor(
fun playSound(soundEvent: SoundEvent) {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(soundEvent, 1.0F))
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/PokedexGUIConstants.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/PokedexGUIConstants.kt
index 7260cef409..f4b479c7c9 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/PokedexGUIConstants.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/PokedexGUIConstants.kt
@@ -35,9 +35,10 @@ object PokedexGUIConstants {
const val TAB_SIZE = 2
const val TAB_STATS = 3
const val TAB_DROPS = 4
+ const val TAB_MOVES = 5
// RowScrollingWidget
const val SCROLL_BASE_HEIGHT = 141
const val SCROLL_SLOT_SIZE = 25
const val SCROLL_SLOT_SPACING = 2
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/widgets/MovesLearnsetWidget.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/widgets/MovesLearnsetWidget.kt
new file mode 100644
index 0000000000..87854a1067
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/widgets/MovesLearnsetWidget.kt
@@ -0,0 +1,1050 @@
+/*
+ * 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.gui.pokedex.widgets
+
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.gui.blitk
+import com.cobblemon.mod.common.api.moves.MoveTemplate
+import com.cobblemon.mod.common.api.pokedex.entry.PokedexForm
+import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
+import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
+import com.cobblemon.mod.common.api.text.bold
+import com.cobblemon.mod.common.api.text.font
+import com.cobblemon.mod.common.api.text.text
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.api.types.ElementalType
+import com.cobblemon.mod.common.client.CobblemonClient
+import com.cobblemon.mod.common.client.CobblemonResources
+import com.cobblemon.mod.common.client.gui.MoveCategoryIcon
+import com.cobblemon.mod.common.client.gui.ScrollingWidget
+import com.cobblemon.mod.common.client.gui.TypeIcon
+import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
+import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
+import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.POKEMON_DESCRIPTION_PADDING
+import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
+import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_BAR_WIDTH
+import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
+import com.cobblemon.mod.common.client.gui.pokedex.renderTooltip
+import com.cobblemon.mod.common.client.settings.ServerSettings
+import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
+import com.cobblemon.mod.common.client.render.drawScaledText
+import com.cobblemon.mod.common.client.render.drawScaledTextJustifiedRight
+import com.cobblemon.mod.common.pokemon.FormData
+import com.cobblemon.mod.common.pokemon.Species
+import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
+import com.cobblemon.mod.common.util.cobblemonResource
+import com.cobblemon.mod.common.util.lang
+import java.math.RoundingMode
+import java.text.DecimalFormat
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.resources.sounds.SimpleSoundInstance
+import net.minecraft.network.chat.Component
+import net.minecraft.network.chat.MutableComponent
+import net.minecraft.network.chat.Style
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.util.FastColor
+import net.minecraft.util.Mth
+
+class MovesLearnsetWidget(val pX: Int, val pY: Int) : SoundlessWidget(
+ pX,
+ pY,
+ HALF_OVERLAY_WIDTH,
+ HALF_OVERLAY_HEIGHT,
+ Component.literal("MovesLearnset")
+) {
+ companion object {
+ private const val LIST_TOP_OFFSET = -15
+ private const val LIST_HEIGHT = 54
+ private const val LIST_SLOT_HEIGHT = 10
+ private const val LIST_SIDE_PADDING = 4
+ private const val LIST_TM_ICON_RENDER_SIZE = 8
+ private const val LIST_TM_ICON_OFFSET = LIST_TM_ICON_RENDER_SIZE + 1
+ private const val LIST_TM_ICON_TEXTURE_SIZE = 16
+ private const val LIST_TYPE_ICON_SIZE = 18
+ private val LIST_BACKGROUND_COLOR = FastColor.ARGB32.color(255, 239, 253, 255)
+
+ private const val DATA_TOP_OFFSET = 108
+ private const val DATA_ROW_HEIGHT = 10
+ private const val DATA_LABEL_OFFSET_X = 14
+ private const val DATA_ICON_SIZE = 10
+ private const val DATA_INFO_TOP_OFFSET = 7
+
+ private const val DESCRIPTION_TOP_OFFSET = 71
+ private const val DESCRIPTION_HEIGHT = 38
+ private const val DESCRIPTION_SCROLLBAR_WIDTH = 3
+ private const val DESCRIPTION_SCROLLBAR_OFFSET = 13
+ private const val DESCRIPTION_LEFT_OFFSET = 62
+ private const val DESCRIPTION_DIVIDER_OFFSET = 6
+
+ private val overlayResource = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
+ private val arrowFormLeft = cobblemonResource("textures/gui/pokedex/forms_arrow_left.png")
+ private val arrowFormRight = cobblemonResource("textures/gui/pokedex/forms_arrow_right.png")
+ private val typeBar = cobblemonResource("textures/gui/pokedex/type_bar.png")
+ private val typeBarDouble = cobblemonResource("textures/gui/pokedex/type_bar_double.png")
+ private val filterArrowLeft = cobblemonResource("textures/gui/pokedex/info_arrow_left.png")
+ private val filterArrowRight = cobblemonResource("textures/gui/pokedex/info_arrow_right.png")
+ private val sortAlphaIcon = cobblemonResource("textures/gui/pokedex/moves_sort_alpha.png")
+ private val sortTypeIcon = cobblemonResource("textures/gui/pokedex/moves_sort_type.png")
+ private val sortSourceIcon = cobblemonResource("textures/gui/pokedex/moves_sort_source.png")
+ private val sortDiscoveredIcon = cobblemonResource("textures/gui/pokedex/moves_sort_discovered.png")
+ private val moveDexTypeIcons = cobblemonResource("textures/gui/pokedex/types_small_dex.png")
+ private val tmDiscIcon = cobblemonResource("textures/item/tms/tm.png")
+ private val movesPowerIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_power.png")
+ private val movesAccuracyIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_accuracy.png")
+ private val movesCategoryIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_category.png")
+
+ // Match PokemonInfoWidget placement
+ private const val TYPE_BAR_Y = 14
+ private const val TYPE_BAR_HEIGHT = 25
+ private const val TYPE_ICON_X = 3
+ private const val TYPE_ICON_Y = 17
+ private const val FORM_LABEL_X = 85
+ private const val FORM_LABEL_Y = 14
+ private const val FORM_ARROW_LEFT_X = 56F
+ private const val FORM_ARROW_RIGHT_X = 107F
+ private const val FORM_ARROW_Y = 15F
+ private const val FORM_ARROW_WIDTH = 10
+ private const val FORM_ARROW_HEIGHT = 16
+
+ // Adjustable filter arrow placement
+ private const val FILTER_ARROW_LEFT_X = 56F
+ private const val FILTER_ARROW_RIGHT_X = 107F
+ private const val FILTER_ARROW_Y = 28F
+ private const val FILTER_ARROW_WIDTH = 7
+ private const val FILTER_ARROW_HEIGHT = 10
+ private const val FILTER_LABEL_X = 84
+ private const val FILTER_LABEL_Y = 26
+
+ // Sort button placement
+ private const val SORT_BUTTON_X = 125F
+ private const val SORT_BUTTON_Y = 27.5F
+ private const val SORT_BUTTON_WIDTH = 20
+ private const val SORT_BUTTON_HEIGHT = 20
+ }
+
+ private val listWidget = LearnsetMovesScrollingWidget(
+ pX + LIST_SIDE_PADDING,
+ pY + LIST_TOP_OFFSET,
+ HALF_OVERLAY_WIDTH - (LIST_SIDE_PADDING * 2),
+ LIST_HEIGHT
+ ) { entry -> selectMove(entry) }
+
+ private val descriptionWidget = MoveDescriptionWidget(
+ pX + LIST_SIDE_PADDING + DESCRIPTION_LEFT_OFFSET,
+ pY + DESCRIPTION_TOP_OFFSET,
+ HALF_OVERLAY_WIDTH - (LIST_SIDE_PADDING * 2) - 50,
+ DESCRIPTION_HEIGHT
+ )
+
+ private var moveEntries: List<LearnsetMoveEntry> = emptyList()
+ private var filteredEntries: List<LearnsetMoveEntry> = emptyList()
+ private var selectedEntry: LearnsetMoveEntry? = null
+ private var speciesName: MutableComponent = Component.literal("")
+ private var speciesNumber: MutableComponent = "0000".text()
+ private var formLabel: MutableComponent = Component.literal("")
+ private var primaryType: ElementalType? = null
+ private var secondaryType: ElementalType? = null
+ private var availableForms: List<PokedexForm> = emptyList()
+ private var onFormChange: ((Boolean) -> Unit)? = null
+ private var filterIndex = 0
+ private var sortMode = LearnsetSort.SOURCE
+
+ private val decimalFormat = DecimalFormat("#.##").also { it.roundingMode = RoundingMode.CEILING }
+
+ private val formLeftButton: ScaledButton = ScaledButton(
+ pX + FORM_ARROW_LEFT_X,
+ pY + FORM_ARROW_Y,
+ FORM_ARROW_WIDTH,
+ FORM_ARROW_HEIGHT,
+ arrowFormLeft,
+ clickAction = { onFormChange?.invoke(false) }
+ ).apply { addWidget(this) }
+
+ private val formRightButton: ScaledButton = ScaledButton(
+ pX + FORM_ARROW_RIGHT_X,
+ pY + FORM_ARROW_Y,
+ FORM_ARROW_WIDTH,
+ FORM_ARROW_HEIGHT,
+ arrowFormRight,
+ clickAction = { onFormChange?.invoke(true) }
+ ).apply { addWidget(this) }
+
+ private val filterLeftButton: ScaledButton = ScaledButton(
+ pX + FILTER_ARROW_LEFT_X,
+ pY + FILTER_ARROW_Y,
+ FILTER_ARROW_WIDTH,
+ FILTER_ARROW_HEIGHT,
+ filterArrowLeft,
+ clickAction = { cycleFilter(false) }
+ ).apply { addWidget(this) }
+
+ private val filterRightButton: ScaledButton = ScaledButton(
+ pX + FILTER_ARROW_RIGHT_X,
+ pY + FILTER_ARROW_Y,
+ FILTER_ARROW_WIDTH,
+ FILTER_ARROW_HEIGHT,
+ filterArrowRight,
+ clickAction = { cycleFilter(true) }
+ ).apply { addWidget(this) }
+
+ private val sortButton: ScaledButton = ScaledButton(
+ pX + SORT_BUTTON_X,
+ pY + SORT_BUTTON_Y,
+ SORT_BUTTON_WIDTH,
+ SORT_BUTTON_HEIGHT,
+ sortSourceIcon,
+ clickAction = { toggleSort() }
+ ).apply { addWidget(this) }
+
+ init {
+ addWidget(listWidget)
+ addWidget(descriptionWidget)
+ }
+
+ fun setLearnset(
+ species: Species,
+ form: FormData,
+ availableForms: List<PokedexForm>,
+ selectedForm: PokedexForm,
+ onFormChange: (Boolean) -> Unit
+ ) {
+ speciesName = species.translatedName
+ speciesNumber = species.nationalPokedexNumber.toString().padStart(4, '0').text()
+ primaryType = form.primaryType
+ secondaryType = form.secondaryType
+ this.availableForms = availableForms
+ this.onFormChange = onFormChange
+ updateFormLabel(species, selectedForm)
+ updateFormButtons()
+
+ moveEntries = buildLearnsetEntries(form)
+ applyFilter()
+ selectMove(null)
+ }
+
+ override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ val matrices = context.pose()
+
+ blitk(
+ matrixStack = matrices,
+ texture = overlayResource,
+ x = pX,
+ y = pY,
+ width = HALF_OVERLAY_WIDTH,
+ height = HALF_OVERLAY_HEIGHT
+ )
+
+ drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = speciesNumber.bold(),
+ x = pX + 3,
+ y = pY + 1,
+ shadow = true
+ )
+
+ if (!speciesName.string.isBlank()) {
+ drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = speciesName.bold(),
+ x = pX + 26,
+ y = pY + 1,
+ colour = 0x606B6E
+ )
+ }
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = if (secondaryType != null) typeBarDouble else typeBar,
+ x = pX,
+ y = pY + TYPE_BAR_Y,
+ width = HALF_OVERLAY_WIDTH,
+ height = TYPE_BAR_HEIGHT
+ )
+
+ if (primaryType != null) {
+ TypeIcon(
+ x = pX + TYPE_ICON_X,
+ y = pY + TYPE_ICON_Y,
+ type = primaryType!!,
+ secondaryType = secondaryType
+ ).render(context)
+ }
+
+ drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = formLabel.bold(),
+ x = pX + FORM_LABEL_X,
+ y = pY + FORM_LABEL_Y,
+ shadow = true,
+ centered = true
+ )
+
+ formLeftButton.render(context, mouseX, mouseY, delta)
+ formRightButton.render(context, mouseX, mouseY, delta)
+
+ drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = currentFilter().label.bold(),
+ x = pX + FILTER_LABEL_X,
+ y = pY + FILTER_LABEL_Y,
+ shadow = true,
+ centered = true
+ )
+
+ filterLeftButton.render(context, mouseX, mouseY, delta)
+ filterRightButton.render(context, mouseX, mouseY, delta)
+ sortButton.render(context, mouseX, mouseY, delta)
+
+ if (sortButton.isButtonHovered(mouseX, mouseY)) {
+ val sortKey = "ui.moves.learnset.sort.${sortMode.name.lowercase()}"
+ renderTooltip(context, lang(sortKey).bold(), mouseX, mouseY, delta, -14)
+ }
+
+ /*drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = lang("ui.moves").bold(),
+ x = pX + 4,
+ y = pY + 11,
+ shadow = true
+ )*/
+
+ renderListBackground(context)
+ if (filteredEntries.isEmpty()) {
+ drawScaledText(
+ context = context,
+ text = Component.literal("No moves available."),
+ x = pX + (HALF_OVERLAY_WIDTH / 2) - 20,
+ y = pY + LIST_TOP_OFFSET + (LIST_HEIGHT / 2) + 30,
+ shadow = false,
+ colour = 0x606B6E,
+ scale = SCALE,
+ centered = true
+ )
+ } else {
+ listWidget.renderWidget(context, mouseX, mouseY, delta)
+ }
+
+ renderDescriptionDivider(context)
+ renderDataSection(context)
+
+ descriptionWidget.renderWidget(context, mouseX, mouseY, delta)
+ }
+
+ private fun renderListBackground(context: GuiGraphics) {
+ val left = pX + LIST_SIDE_PADDING - 3
+ val top = pY + LIST_TOP_OFFSET + 54
+ val right = left + listWidget.width + 6
+ val bottom = top + LIST_HEIGHT
+ context.fill(left, top, right, bottom, LIST_BACKGROUND_COLOR)
+ }
+
+ private fun renderDescriptionDivider(context: GuiGraphics) {
+ val dividerX = pX + LIST_SIDE_PADDING + DESCRIPTION_LEFT_OFFSET + DESCRIPTION_DIVIDER_OFFSET
+ val dividerTop = pY + DESCRIPTION_TOP_OFFSET + 39
+ val dividerBottom = pY + DESCRIPTION_TOP_OFFSET + DESCRIPTION_HEIGHT + 37
+ context.fill(dividerX, dividerTop, dividerX + 1, dividerBottom, FastColor.ARGB32.color(255, 126, 231, 229))
+ }
+
+ private fun renderDataSection(context: GuiGraphics) {
+ val dataTop = pY + DATA_TOP_OFFSET
+ val dataRight = pX + HALF_OVERLAY_WIDTH - 70
+ val entry = selectedEntry
+ val move = entry?.move
+ val showMoveInfo = entry?.isDiscovered == true
+
+ drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = Component.literal("Data").bold(),
+ x = pX + 4,
+ y = dataTop - 10,
+ shadow = true
+ )
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = movesPowerIconResource,
+ x = (pX + 6) / SCALE,
+ y = (dataTop + 1 + DATA_INFO_TOP_OFFSET) / SCALE,
+ width = DATA_ICON_SIZE,
+ height = DATA_ICON_SIZE,
+ scale = SCALE
+ )
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = movesAccuracyIconResource,
+ x = (pX + 6) / SCALE,
+ y = (dataTop + 1 + DATA_ROW_HEIGHT + DATA_INFO_TOP_OFFSET) / SCALE,
+ width = DATA_ICON_SIZE,
+ height = DATA_ICON_SIZE,
+ scale = SCALE
+ )
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = movesCategoryIconResource,
+ x = (pX + 6) / SCALE,
+ y = (dataTop + 1 + (DATA_ROW_HEIGHT * 2) + DATA_INFO_TOP_OFFSET) / SCALE,
+ width = DATA_ICON_SIZE,
+ height = DATA_ICON_SIZE,
+ scale = SCALE
+ )
+
+ drawScaledText(
+ context = context,
+ text = lang("ui.power"),
+ x = pX + DATA_LABEL_OFFSET_X,
+ y = dataTop + 1 + DATA_INFO_TOP_OFFSET,
+ scale = SCALE,
+ shadow = true
+ )
+
+ drawScaledText(
+ context = context,
+ text = lang("ui.accuracy"),
+ x = pX + DATA_LABEL_OFFSET_X,
+ y = dataTop + 1 + DATA_ROW_HEIGHT + DATA_INFO_TOP_OFFSET,
+ scale = SCALE,
+ shadow = true
+ )
+
+ drawScaledText(
+ context = context,
+ text = Component.literal("ui.category"),
+ x = pX + DATA_LABEL_OFFSET_X,
+ y = dataTop + 1 + (DATA_ROW_HEIGHT * 2) + DATA_INFO_TOP_OFFSET,
+ scale = SCALE,
+ shadow = true
+ )
+
+ val powerText = if (showMoveInfo && move != null && move.power.toInt() > 0) move.power.toInt().toString() else "-"
+ drawScaledTextJustifiedRight(
+ context = context,
+ text = powerText.text(),
+ x = dataRight,
+ y = dataTop + 1 + DATA_INFO_TOP_OFFSET,
+ scale = SCALE,
+ shadow = true
+ )
+
+ val accuracyText = if (showMoveInfo && move != null) formatAccuracy(move.accuracy) else "-"
+ drawScaledTextJustifiedRight(
+ context = context,
+ text = accuracyText.text(),
+ x = dataRight,
+ y = dataTop + 1 + DATA_ROW_HEIGHT + DATA_INFO_TOP_OFFSET,
+ scale = SCALE,
+ shadow = true
+ )
+
+ val categoryText = if (showMoveInfo && move != null) move.damageCategory.displayName else "-".text()
+ drawScaledTextJustifiedRight(
+ context = context,
+ text = categoryText as MutableComponent,
+ x = dataRight,
+ y = dataTop + 1 + (DATA_ROW_HEIGHT * 2) + DATA_INFO_TOP_OFFSET,
+ scale = SCALE,
+ shadow = true
+ )
+
+ /*if (showMoveInfo && move != null) {
+ MoveCategoryIcon(
+ x = pX + 6,
+ y = dataTop + 2 + (DATA_ROW_HEIGHT * 2) + DATA_INFO_TOP_OFFSET,
+ category = move.damageCategory
+ ).render(context)
+ }*/
+ }
+
+ private fun formatAccuracy(input: Double): String {
+ if (input <= 0) return "-"
+ return "${decimalFormat.format(input)}%"
+ }
+
+ private fun applyFilter() {
+ val filtered = when (currentFilter()) {
+ LearnsetFilter.ALL -> moveEntries
+ LearnsetFilter.LEVEL_UP -> moveEntries.filter { it.source == LearnsetSource.LEVEL_UP }
+ LearnsetFilter.TM -> moveEntries.filter { it.source == LearnsetSource.TM }
+ LearnsetFilter.EGG -> moveEntries.filter { it.source == LearnsetSource.EGG }
+ }
+ val sorted = sortEntries(filtered)
+ filteredEntries = sorted
+ listWidget.setEntries(sorted)
+ if (selectedEntry !in sorted) {
+ selectMove(null)
+ } else {
+ listWidget.setSelectedEntry(selectedEntry)
+ }
+ }
+
+ private fun sortEntries(entries: List<LearnsetMoveEntry>): List<LearnsetMoveEntry> {
+ val baseEntries = if (sortMode == LearnsetSort.DISCOVERED) {
+ entries.filter { it.isDiscovered }
+ } else {
+ entries
+ }
+ return when (sortMode) {
+ LearnsetSort.ALPHABETICAL -> baseEntries.sortedWith(
+ compareBy(
+ { it.move.displayName.string.lowercase() },
+ { it.level ?: Int.MAX_VALUE }
+ )
+ )
+ LearnsetSort.TYPE -> baseEntries.sortedWith(
+ compareBy(
+ { it.move.elementalType.displayName.string.lowercase() },
+ { it.move.displayName.string.lowercase() },
+ { it.level ?: Int.MAX_VALUE }
+ )
+ )
+ LearnsetSort.SOURCE,
+ LearnsetSort.DISCOVERED -> baseEntries.sortedWith(
+ compareBy(
+ { sourceOrder(it.source) },
+ { it.level ?: Int.MAX_VALUE },
+ { it.move.displayName.string.lowercase() }
+ )
+ )
+ }
+ }
+
+ private fun cycleFilter(next: Boolean) {
+ val total = LearnsetFilter.entries.size
+ filterIndex = if (next) {
+ (filterIndex + 1) % total
+ } else {
+ (filterIndex - 1 + total) % total
+ }
+ applyFilter()
+ }
+
+ private fun toggleSort() {
+ sortMode = when (sortMode) {
+ LearnsetSort.ALPHABETICAL -> LearnsetSort.TYPE
+ LearnsetSort.TYPE -> LearnsetSort.SOURCE
+ LearnsetSort.SOURCE -> LearnsetSort.DISCOVERED
+ LearnsetSort.DISCOVERED -> LearnsetSort.ALPHABETICAL
+ }
+ sortButton.resource = when (sortMode) {
+ LearnsetSort.ALPHABETICAL -> sortAlphaIcon
+ LearnsetSort.TYPE -> sortTypeIcon
+ LearnsetSort.SOURCE -> sortSourceIcon
+ LearnsetSort.DISCOVERED -> sortDiscoveredIcon
+ }
+ applyFilter()
+ }
+
+ private fun sourceOrder(source: LearnsetSource): Int {
+ return when (source) {
+ LearnsetSource.LEVEL_UP -> 0
+ LearnsetSource.TM -> 1
+ LearnsetSource.EGG -> 2
+ }
+ }
+
+ private fun currentFilter(): LearnsetFilter {
+ return LearnsetFilter.entries[filterIndex.coerceIn(0, LearnsetFilter.entries.lastIndex)]
+ }
+
+ private fun updateFormButtons() {
+ val hasForms = availableForms.size > 1
+ formLeftButton.isVisible = hasForms
+ formRightButton.isVisible = hasForms
+ formLeftButton.active = hasForms
+ formRightButton.active = hasForms
+ }
+
+ private fun updateFormLabel(species: Species, form: PokedexForm) {
+ val formName = if (form.displayForm.equals("normal", ignoreCase = true)) {
+ ""
+ } else {
+ "-${form.displayForm.lowercase().replace("-", "")}"
+ }
+ val label = lang("ui.pokedex.info.form.${species}${formName}")
+ formLabel = if (label.string.isBlank() || label.string.startsWith("cobblemon.ui.pokedex.info.form.")) {
+ Component.literal(form.displayForm)
+ } else {
+ label
+ }
+ }
+
+ private fun selectMove(entry: LearnsetMoveEntry?) {
+ selectedEntry = if (selectedEntry == entry || entry == null) null else entry
+ listWidget.setSelectedEntry(selectedEntry)
+
+ if (selectedEntry?.isDiscovered == true) {
+ val description = selectedEntry!!.move.description.string
+ descriptionWidget.showPlaceholder = false
+ descriptionWidget.setText(listOf(description))
+ } else {
+ descriptionWidget.showPlaceholder = true
+ descriptionWidget.setText(emptyList())
+ }
+ }
+
+ private fun buildEvolutionMoveLevelIndex(form: FormData): Map<ResourceLocation, Map<String, Int>> {
+ val evolutionForms = collectEvolutionForms(form)
+ val levelsBySpecies = mutableMapOf<ResourceLocation, MutableMap<String, Int>>()
+
+ for (evolutionForm in evolutionForms) {
+ val speciesId = evolutionForm.species.resourceIdentifier
+ val moveLevels = levelsBySpecies.getOrPut(speciesId) { mutableMapOf() }
+ buildMoveLevelIndex(evolutionForm).forEach { (moveName, level) ->
+ val current = moveLevels[moveName]
+ if (current == null || level < current) {
+ moveLevels[moveName] = level
+ }
+ }
+ }
+
+ return levelsBySpecies
+ }
+
+ private fun buildMoveLevelIndex(form: FormData): Map<String, Int> {
+ val levels = mutableMapOf<String, Int>()
+ form.moves.levelUpMoves.forEach { (level, moves) ->
+ moves.forEach { move ->
+ val current = levels[move.name]
+ if (current == null || level < current) {
+ levels[move.name] = level
+ }
+ }
+ }
+ return levels
+ }
+
+ private fun collectEvolutionForms(rootForm: FormData): List<FormData> {
+ val results = mutableListOf<FormData>()
+ val visited = mutableSetOf<String>()
+
+ fun key(form: FormData): String {
+ return "${form.species.resourceIdentifier}|${form.name.lowercase()}"
+ }
+
+ fun nextEvolutions(current: FormData): Set<Evolution> {
+ return if (current.evolutions.isNotEmpty()) {
+ current.evolutions
+ } else {
+ current.species.evolutions
+ }
+ }
+
+ fun traverse(current: FormData) {
+ for (evolution in nextEvolutions(current)) {
+ val evolutionForm = resolveEvolutionForm(evolution) ?: continue
+ val evolutionKey = key(evolutionForm)
+ if (!visited.add(evolutionKey)) continue
+ results.add(evolutionForm)
+ traverse(evolutionForm)
+ }
+ }
+
+ traverse(rootForm)
+ return results
+ }
+
+ private fun resolveEvolutionForm(evolution: Evolution): FormData? {
+ val speciesId = evolution.result.species?.asIdentifierDefaultingNamespace() ?: return null
+ val species = PokemonSpecies.getByIdentifier(speciesId) ?: return null
+ val formId = evolution.result.form
+ return if (formId == null) {
+ species.standardForm
+ } else {
+ species.forms.firstOrNull {
+ it.formOnlyShowdownId().equals(formId, ignoreCase = true) || it.name.equals(formId, ignoreCase = true)
+ } ?: species.standardForm
+ }
+ }
+
+ private fun buildLearnsetEntries(form: FormData): List<LearnsetMoveEntry> {
+ val entries = linkedMapOf<String, LearnsetMoveEntry>()
+ val speciesId = form.species.resourceIdentifier
+ val pokedexManager = CobblemonClient.clientPokedexData
+ val highestLevel = pokedexManager.getSpeciesRecord(speciesId)?.getFormRecord(form.name)?.highestLevel ?: 0
+ val evolutionMoveLevels = buildEvolutionMoveLevelIndex(form)
+ val learnedTMs = CobblemonClient.clientTMMoveData.learnedTMs
+ val unlockAllMoveDexMovesByDefault = ServerSettings.unlockAllMoveDexMovesByDefault
+
+ fun isLevelUpDiscovered(move: MoveTemplate, level: Int): Boolean {
+ if (unlockAllMoveDexMovesByDefault) return true
+ if (highestLevel >= level) return true
+
+ for ((evolutionSpeciesId, moveLevels) in evolutionMoveLevels) {
+ val evolutionLevel = moveLevels[move.name] ?: continue
+ val evolutionHighest = pokedexManager.getSpeciesRecord(evolutionSpeciesId)?.highestLevel ?: 0
+ if (evolutionHighest >= evolutionLevel) return true
+ }
+
+ return false
+ }
+
+ fun addEntry(
+ move: MoveTemplate,
+ source: LearnsetSource,
+ level: Int? = null,
+ tmLocked: Boolean = false,
+ isDiscovered: Boolean = true
+ ) {
+ val key = move.name
+ if (!entries.containsKey(key)) {
+ val tmId = TechnicalMachines.moveToTM[move]?.id
+ val tmUnlocked = source == LearnsetSource.TM && tmId != null && tmId in learnedTMs
+ val resolvedTmLocked = if (source == LearnsetSource.TM) tmLocked else false
+ entries[key] = LearnsetMoveEntry(move, source, level, resolvedTmLocked, isDiscovered, tmId, tmUnlocked)
+ }
+ }
+
+ form.moves.levelUpMoves.toSortedMap().forEach { (level, moves) ->
+ moves.forEach { move ->
+ val discovered = isLevelUpDiscovered(move, level)
+ addEntry(move, LearnsetSource.LEVEL_UP, level = level, isDiscovered = discovered)
+ }
+ }
+
+ form.moves.tmMoves.sortedBy { it.displayName.string }.forEach { move ->
+ val tmLocked = if (unlockAllMoveDexMovesByDefault) false else {
+ val tmId = TechnicalMachines.moveToTM[move]?.id
+ tmId != null && tmId !in learnedTMs
+ }
+ addEntry(move, LearnsetSource.TM, tmLocked = tmLocked, isDiscovered = !tmLocked)
+ }
+
+ form.moves.eggMoves.sortedBy { it.displayName.string }.forEach { move ->
+ addEntry(move, LearnsetSource.EGG)
+ }
+
+ return entries.values.toList()
+ }
+
+ private enum class LearnsetSource(val label: String) {
+ LEVEL_UP("Lv"),
+ TM("TM"),
+ EGG("Egg")
+ }
+
+ private enum class LearnsetFilter(val label: MutableComponent) {
+ ALL(Component.literal("All")),
+ LEVEL_UP(Component.literal("Level-Up")),
+ TM(Component.literal("TM")),
+ EGG(Component.literal("Egg"))
+ }
+
+ private enum class LearnsetSort {
+ ALPHABETICAL,
+ TYPE,
+ SOURCE,
+ DISCOVERED
+ }
+
+ private data class LearnsetMoveEntry(
+ val move: MoveTemplate,
+ val source: LearnsetSource,
+ val level: Int? = null,
+ val tmLocked: Boolean = false,
+ val isDiscovered: Boolean = true,
+ val tmId: ResourceLocation? = null,
+ val tmUnlocked: Boolean = false
+ )
+
+ // todo fix the scrolling area height
+ private class LearnsetMovesScrollingWidget(
+ val pX: Int,
+ val pY: Int,
+ listWidth: Int,
+ listHeight: Int,
+ val onSelect: (LearnsetMoveEntry?) -> Unit
+ ) : ScrollingWidget<LearnsetMovesScrollingWidget.MoveEntrySlot>(
+ width = listWidth,
+ height = listHeight,
+ left = pX,
+ top = pY,
+ slotHeight = LIST_SLOT_HEIGHT
+ ) {
+ private var selectedEntry: LearnsetMoveEntry? = null
+
+ fun setEntries(entries: List<LearnsetMoveEntry>) {
+ clearEntries()
+ entries.forEach { entry -> addEntry(MoveEntrySlot(entry, this)) }
+ scrollAmount = 0.0
+ }
+
+
+
+ fun setSelectedEntry(entry: LearnsetMoveEntry?) {
+ selectedEntry = entry
+ }
+
+ override fun getScrollbarPosition(): Int {
+ return left + width - 3
+ }
+
+ override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ val xLeft = this.scrollbarPosition
+ val xRight = xLeft + 3
+ val yStart = y + 1
+
+ val barHeight = this.bottom - yStart
+
+ var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
+ yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
+ var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
+ if (yTop < yStart) yTop = yStart
+
+ context.fill(xLeft + 1, yStart, xRight - 1, this.bottom, FastColor.ARGB32.color(255, 126, 231, 229)) // track
+ context.fill(xLeft, yTop, xRight, yTop + yBottom, FastColor.ARGB32.color(255, 58, 150, 182)) // bar
+ }
+
+ override fun getEntry(index: Int): MoveEntrySlot {
+ return children()[index] as MoveEntrySlot
+ }
+
+ inner class MoveEntrySlot(
+ val entry: LearnsetMoveEntry,
+ private val parentList: LearnsetMovesScrollingWidget
+ ) : Slot<MoveEntrySlot>() {
+ private var lastX = 0
+ private var lastY = 0
+
+ override fun render(
+ context: GuiGraphics,
+ index: Int,
+ y: Int,
+ x: Int,
+ entryWidth: Int,
+ entryHeight: Int,
+ mouseX: Int,
+ mouseY: Int,
+ hovered: Boolean,
+ tickDelta: Float
+ ) {
+ lastX = x
+ lastY = y
+
+ val isSelected = entry == parentList.selectedEntry
+ if (hovered || isSelected) {
+ val color = if (isSelected) {
+ FastColor.ARGB32.color(140, 58, 150, 182)
+ } else {
+ FastColor.ARGB32.color(90, 126, 231, 229)
+ }
+ context.fill(x, y, x + entryWidth - 3, y + entryHeight, color)
+ }
+
+ val leftOffset = LIST_TM_ICON_OFFSET
+ if (entry.tmId != null) {
+ val (red, green, blue) = if (entry.tmUnlocked) {
+ Triple(0.32F, 0.82F, 0.46F)
+ } else {
+ Triple(0.6F, 0.6F, 0.6F)
+ }
+ val iconX = x + 1
+ val iconY = y + 1
+ blitk(
+ matrixStack = context.pose(),
+ texture = tmDiscIcon,
+ x = iconX / SCALE,
+ y = iconY / SCALE,
+ width = LIST_TM_ICON_TEXTURE_SIZE,
+ height = LIST_TM_ICON_TEXTURE_SIZE,
+ red = red,
+ green = green,
+ blue = blue,
+ alpha = 1F,
+ scale = SCALE
+ )
+ val hoveringIcon = mouseX in iconX..(iconX + LIST_TM_ICON_RENDER_SIZE) &&
+ mouseY in iconY..(iconY + LIST_TM_ICON_RENDER_SIZE)
+ if (hoveringIcon) {
+ val tooltipKey = if (entry.tmUnlocked) {
+ "ui.moves.learnset.tm.discovered"
+ } else {
+ "egg_group.undiscovered"
+ }
+ val tooltipText = lang(tooltipKey)
+ val textWidth = Minecraft.getInstance().font.width(tooltipText.font(CobblemonResources.DEFAULT_LARGE))
+ val tooltipWidth = textWidth + 6
+ val listLeft = parentList.left + 2
+ val listRight = parentList.left + parentList.width - 6
+ val minCenter = listLeft + (tooltipWidth / 2)
+ val maxCenter = listRight - (tooltipWidth / 2)
+ val tooltipX = if (minCenter <= maxCenter) {
+ Mth.clamp(mouseX, minCenter, maxCenter)
+ } else {
+ mouseX
+ }
+ renderTooltip(context, tooltipText, tooltipX, mouseY, tickDelta, -14)
+ }
+ }
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = moveDexTypeIcons,
+ x = (x + 1 + leftOffset) / SCALE,
+ y = (y + 1) / SCALE,
+ width = LIST_TYPE_ICON_SIZE,
+ height = LIST_TYPE_ICON_SIZE,
+ uOffset = LIST_TYPE_ICON_SIZE * entry.move.elementalType.textureXMultiplier.toFloat() + 0.1,
+ textureWidth = LIST_TYPE_ICON_SIZE * 18,
+ textureHeight = LIST_TYPE_ICON_SIZE,
+ scale = SCALE
+ )
+
+ val displayName = if (entry.isDiscovered) {
+ entry.move.displayName
+ } else {
+ Component.literal("?????")
+ }
+
+ drawScaledText(
+ context = context,
+ text = displayName,
+ x = x + 12 + leftOffset,
+ y = y + 2,
+ scale = SCALE,
+ shadow = false,
+ colour = if (entry.isDiscovered) 0x606B6E else 0x8A8F91
+ )
+
+ val rightLabel = when {
+ entry.level != null -> "Lv.${entry.level}"
+ entry.source == LearnsetSource.TM -> entry.source.label
+ else -> entry.source.label
+ }
+
+ drawScaledTextJustifiedRight(
+ context = context,
+ text = rightLabel.text(),
+ x = x + entryWidth - 6,
+ y = y + 2,
+ scale = SCALE,
+ shadow = false,
+ colour = if (entry.tmLocked || !entry.isDiscovered) 0x8A8F91 else 0x606B6E
+ )
+
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ val canClick = mouseX < (lastX + (parentList.width - 3))
+ if (canClick) {
+ onSelect(entry)
+ Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.POKEDEX_CLICK_SHORT, 1.0F))
+ }
+ return true
+ }
+
+ override fun getNarration(): Component {
+ return if (entry.isDiscovered) entry.move.displayName else Component.literal("?????")
+ }
+ }
+ }
+
+ private class MoveDescriptionWidget(
+ val pX: Int,
+ val pY: Int,
+ width: Int,
+ height: Int
+ ) : ScrollingWidget<MoveDescriptionWidget.TextSlot>(
+ left = pX,
+ top = pY,
+ width = width,
+ height = height,
+ slotHeight = LIST_SLOT_HEIGHT,
+ scrollBarWidth = DESCRIPTION_SCROLLBAR_WIDTH
+ ) {
+ var showPlaceholder = true
+
+ override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ if (showPlaceholder) {
+ /*drawScaledText(
+ context = context,
+ text = Component.literal("Select a move."),
+ x = pX + (width / 2),
+ y = pY + (height / 2) - 3,
+ shadow = false,
+ colour = 0x606B6E,
+ scale = SCALE,
+ centered = true
+ )*/
+ return
+ }
+ super.renderWidget(context, mouseX, mouseY, delta)
+ }
+
+ fun setText(text: Collection<String>) {
+ clearEntries()
+ text.forEach { line ->
+ Minecraft.getInstance().font.splitter.splitLines(
+ Component.literal(line),
+ ((width - DESCRIPTION_SCROLLBAR_OFFSET - (POKEMON_DESCRIPTION_PADDING * 2)) / SCALE).toInt(),
+ Style.EMPTY
+ ).stream()
+ .map { it.string }
+ .forEach { addEntry(TextSlot(it)) }
+ }
+ scrollAmount = 0.0
+ }
+
+ override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ val xLeft = this.scrollbarPosition
+ val xRight = xLeft + scrollBarWidth
+ val yStart = y + 1
+
+ val barHeight = this.bottom - yStart
+
+ var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
+ yBottom = Mth.clamp(yBottom, 16, barHeight - 6)
+ var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
+ if (yTop < yStart) yTop = yStart
+
+ context.fill(xLeft + 1, yStart, xRight - 1, this.bottom, FastColor.ARGB32.color(255, 126, 231, 229)) // track
+ context.fill(xLeft, yTop, xRight, yTop + yBottom, FastColor.ARGB32.color(255, 58, 150, 182)) // bar
+ }
+
+ override fun getScrollbarPosition(): Int {
+ return left + width - DESCRIPTION_SCROLLBAR_OFFSET
+ }
+
+ class TextSlot(val text: String) : Slot<TextSlot>() {
+ override fun render(
+ context: GuiGraphics,
+ index: Int,
+ y: Int,
+ x: Int,
+ entryWidth: Int,
+ entryHeight: Int,
+ mouseX: Int,
+ mouseY: Int,
+ hovered: Boolean,
+ tickDelta: Float
+ ) {
+ drawScaledText(
+ context = context,
+ text = text.text(),
+ x = x + POKEMON_DESCRIPTION_PADDING,
+ y = y + 2,
+ scale = SCALE,
+ shadow = false,
+ colour = 0x606B6E
+ )
+ }
+
+ override fun getNarration(): Component {
+ return Component.literal(text)
+ }
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/widgets/PokemonInfoWidget.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/widgets/PokemonInfoWidget.kt
index 770faadeaa..784de205ee 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/widgets/PokemonInfoWidget.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/pokedex/widgets/PokemonInfoWidget.kt
@@ -125,6 +125,7 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
var ticksElapsed = 0
var pokeBallBackgroundFrame = 0
+ var isSuppressed = false
class VariationButtonWrapper(
val parent: PokemonInfoWidget,
@@ -259,6 +260,9 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
).apply { addWidget(this) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ if (isSuppressed) {
+ return
+ }
val currentEntry = this.currentEntry ?: return
val hasKnowledge = CobblemonClient.clientPokedexData.getKnowledgeForSpecies(currentEntry.speciesId) != PokedexEntryProgress.NONE
@@ -512,6 +516,40 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
matrices.popPose()
}
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ if (isSuppressed) {
+ return false
+ }
+ return super.mouseClicked(mouseX, mouseY, button)
+ }
+
+ override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ if (isSuppressed) {
+ return false
+ }
+ return super.mouseReleased(mouseX, mouseY, button)
+ }
+
+ override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
+ if (isSuppressed) {
+ return false
+ }
+ return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
+ }
+
+ override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
+ if (isSuppressed) {
+ return false
+ }
+ return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
+ }
+
+ override fun mouseMoved(mouseX: Double, mouseY: Double) {
+ if (!isSuppressed) {
+ super.mouseMoved(mouseX, mouseY)
+ }
+ }
+
fun setDexEntry(pokedexEntry : PokedexEntry) {
this.currentEntry = pokedexEntry
this.renderablePokemon = null
@@ -624,6 +662,19 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
updateAspects()
}
+ fun setSelectedForm(newForm: PokedexForm) {
+ if (visibleForms.isEmpty() || currentEntry == null) {
+ return
+ }
+ val index = visibleForms.indexOfFirst { it.displayForm.equals(newForm.displayForm, ignoreCase = true) }
+ if (index == -1) {
+ return
+ }
+ selectedFormIndex = index
+ setupButtons(currentEntry!!, visibleForms[selectedFormIndex])
+ updateAspects()
+ }
+
fun updateAspects() {
if (visibleForms.isEmpty()) {
return
@@ -706,4 +757,4 @@ class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm)
fun playSound(soundEvent: SoundEvent) {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(soundEvent, 1.0F))
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MoveSlotWidget.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MoveSlotWidget.kt
index a53cb8eca2..f7ca2e10c2 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MoveSlotWidget.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MoveSlotWidget.kt
@@ -36,9 +36,9 @@ class MoveSlotWidget(
): SoundlessWidget(pX, pY, MOVE_WIDTH, MOVE_HEIGHT, Component.literal(move?.name ?: "")) {
companion object {
- private val moveResource = cobblemonResource("textures/gui/summary/summary_move.png")
- private val moveOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay.png")
- private val moveSelectedOverlayResource = cobblemonResource("textures/gui/summary/summary_move_selected_overlay.png")
+ val moveResource = cobblemonResource("textures/gui/summary/summary_move.png")
+ val moveOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay.png")
+ val moveSelectedOverlayResource = cobblemonResource("textures/gui/summary/summary_move_selected_overlay.png")
const val MOVE_WIDTH = 108
const val MOVE_HEIGHT = 22
@@ -50,6 +50,7 @@ class MoveSlotWidget(
}.apply {
addWidget(this)
}
+
private val moveDownButton = ReorderMoveButton(x, y, false) {
movesWidget.selectMove(null)
movesWidget.reorderMove(this, false)
@@ -67,46 +68,46 @@ class MoveSlotWidget(
}.apply {
addWidget(this)
}
+
val elementalType:ElementalType = Moves.getByNameOrDummy(move?.name ?: "").getEffectiveElementalType(pokemon)
override fun renderWidget(context: GuiGraphics, pMouseX: Int, pMouseY: Int, pPartialTicks: Float) {
val matrices = context.pose()
isHovered = pMouseX >= x && pMouseY >= y && pMouseX < x + width && pMouseY < y + height
if (move != null) {
- val moveTemplate = Moves.getByNameOrDummy(move.name)
val rgb = elementalType.hue.toRGB()
if (movesWidget.selectedMove == move) {
blitk(
- matrixStack = matrices,
- texture = moveSelectedOverlayResource,
- x = x - 1,
- y = y - 1,
- width = MOVE_WIDTH + 2,
- height = MOVE_HEIGHT + 2
+ matrixStack = matrices,
+ texture = moveSelectedOverlayResource,
+ x = x - 1,
+ y = y - 1,
+ width = MOVE_WIDTH + 2,
+ height = MOVE_HEIGHT + 2
)
}
blitk(
- matrixStack = matrices,
- texture = moveResource,
- x = x,
- y = y,
- width = MOVE_WIDTH,
- height = MOVE_HEIGHT,
- vOffset = if (isHovered) MOVE_HEIGHT else 0,
- textureHeight = MOVE_HEIGHT * 2,
- red = rgb.first,
- green = rgb.second,
- blue = rgb.third
+ matrixStack = matrices,
+ texture = moveResource,
+ x = x,
+ y = y,
+ width = MOVE_WIDTH,
+ height = MOVE_HEIGHT,
+ vOffset = if (isHovered) MOVE_HEIGHT else 0,
+ textureHeight = MOVE_HEIGHT * 2,
+ red = rgb.first,
+ green = rgb.second,
+ blue = rgb.third
)
blitk(
- matrixStack = matrices,
- texture = moveOverlayResource,
- x = x,
- y = y,
- width = MOVE_WIDTH,
- height = MOVE_HEIGHT
+ matrixStack = matrices,
+ texture = moveOverlayResource,
+ x = x,
+ y = y,
+ width = MOVE_WIDTH,
+ height = MOVE_HEIGHT
)
var movePPText = Component.literal("${move.currentPp}/${move.maxPp}").bold()
@@ -116,36 +117,28 @@ class MoveSlotWidget(
}
drawScaledText(
- context = context,
- font = CobblemonResources.DEFAULT_LARGE,
- text = movePPText,
- x = x + 93,
- y = y + 13,
- centered = true
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = movePPText,
+ x = x + 93,
+ y = y + 13,
+ centered = true
)
// Type Icon
- TypeIcon(
- x = x + 2,
- y = y + 2,
- type = elementalType
- ).render(context)
+ TypeIcon(x = x + 2, y = y + 2, type = elementalType).render(context)
// Move Category
- MoveCategoryIcon(
- x = x + 66,
- y = y + 13.5,
- category = move.damageCategory
- ).render(context)
+ MoveCategoryIcon(x = x + 66, y = y + 13.5, category = move.damageCategory).render(context)
// Move Name
drawScaledText(
- context = context,
- font = CobblemonResources.DEFAULT_LARGE,
- text = move.displayName.bold(),
- x = x + 28,
- y = y + 2,
- shadow = true
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = move.displayName.bold(),
+ x = x + 28,
+ y = y + 2,
+ shadow = true
)
// Reorder Buttons
@@ -158,9 +151,7 @@ class MoveSlotWidget(
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
- if (isHovered) {
- movesWidget.selectMove(move)
- }
+ if (isHovered) movesWidget.selectMove(move)
return super.mouseClicked(mouseX, mouseY, button)
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MoveSwapScreen.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MoveSwapScreen.kt
index 2f80532e5c..453cd567d7 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MoveSwapScreen.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MoveSwapScreen.kt
@@ -162,7 +162,7 @@ class MoveSwapScreen(
drawScaledText(
context = context,
- text = pane.movesWidget.format(move.accuracy).text(),
+ text = MovesWidget.format(move.accuracy).text(),
x = rowLeft + 37,
y = tweakedRowTop + 12,
scale = MovesWidget.SCALE,
@@ -171,7 +171,7 @@ class MoveSwapScreen(
drawScaledText(
context = context,
- text = pane.movesWidget.format(move.effectChances.firstOrNull() ?: 0.0).text(),
+ text = MovesWidget.format(move.effectChances.firstOrNull() ?: 0.0).text(),
x = rowLeft + 60.5,
y = tweakedRowTop + 12,
scale = MovesWidget.SCALE,
@@ -232,4 +232,4 @@ class MoveSwapScreen(
return false
}
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MovesWidget.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MovesWidget.kt
index 2b47353421..e12ffc65fd 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MovesWidget.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/summary/widgets/screens/moves/MovesWidget.kt
@@ -39,6 +39,11 @@ class MovesWidget(
it.roundingMode = RoundingMode.CEILING
}
+ fun format(input: Double): String {
+ if (input <= 0) return "ΓÇö"
+ return "${decimalFormat.format(input)}%"
+ }
+
private val movesBaseResource = cobblemonResource("textures/gui/summary/summary_moves_base.png")
val movesPowerIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_power.png")
val movesAccuracyIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_accuracy.png")
@@ -208,11 +213,6 @@ class MovesWidget(
)
}
- fun format(input: Double): String {
- if (input <= 0) return "ΓÇö"
- return "${decimalFormat.format(input)}%"
- }
-
fun selectMove(move: Move?) {
selectedMove = if (selectedMove == move || move == null) null else move
descriptionScrollList.setMoveDescription(selectedMove?.description ?: "".text())
@@ -232,4 +232,4 @@ class MovesWidget(
if (descriptionScrollList.isHovered) descriptionScrollList.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/IconButton.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/IconButton.kt
new file mode 100644
index 0000000000..e144079a42
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/IconButton.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.gui.tmmachine
+
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.gui.blitk
+import com.cobblemon.mod.common.api.text.text
+import com.cobblemon.mod.common.client.gui.CobblemonRenderable
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.components.Button
+import net.minecraft.client.gui.narration.NarrationElementOutput
+import net.minecraft.client.resources.sounds.SimpleSoundInstance
+import net.minecraft.client.sounds.SoundManager
+import net.minecraft.resources.ResourceLocation
+
+class IconButton(
+ var buttonX: Float,
+ var buttonY: Float,
+ val buttonWidth: Number,
+ val buttonHeight: Number,
+ var resource: ResourceLocation? = null,
+ val scale: Float = 0.5F,
+ val hoverExtendRight: Int = 0,
+ val silent: Boolean = false,
+ val clickAction: OnPress
+): Button(buttonX.toInt(), buttonY.toInt(), buttonWidth.toInt(), buttonHeight.toInt(), "".text(), clickAction, DEFAULT_NARRATION), CobblemonRenderable {
+
+ override fun mouseDragged(d: Double, e: Double, i: Int, f: Double, g: Double) = false
+ override fun defaultButtonNarrationText(builder: NarrationElementOutput) {}
+
+ override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) {
+ resource?.let {
+ blitk(
+ matrixStack = context.pose(),
+ texture = it,
+ x = buttonX / scale,
+ y = buttonY / scale,
+ width = buttonWidth,
+ height = buttonHeight,
+ vOffset = if (isMouseOver(mouseX.toDouble(), mouseY.toDouble())) buttonHeight else 0,
+ textureHeight = buttonHeight.toFloat() * 2,
+ scale = scale
+ )
+ }
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ if (active && visible && button == 0 && isMouseOver(mouseX, mouseY)) {
+ playDownSound(Minecraft.getInstance().soundManager)
+ onClick(mouseX, mouseY)
+ return true
+ }
+ return false
+ }
+
+ override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
+ val minX = x.toDouble()
+ val maxX = minX + width + hoverExtendRight
+ val minY = y.toDouble()
+ val maxY = minY + (height * scale)
+ return active && visible && mouseX >= minX && mouseX < maxX && mouseY >= minY && mouseY < maxY
+ }
+
+ override fun playDownSound(soundManager: SoundManager) {
+ if (visible && !this.silent) soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/MoveSearchWidget.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/MoveSearchWidget.kt
new file mode 100644
index 0000000000..41b03f81d8
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/MoveSearchWidget.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.gui.tmmachine
+
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.gui.blitk
+import com.cobblemon.mod.common.api.text.bold
+import com.cobblemon.mod.common.api.text.text
+import com.cobblemon.mod.common.client.CobblemonResources
+import com.cobblemon.mod.common.client.gui.pc.TextWidget
+import com.cobblemon.mod.common.client.render.drawScaledText
+import com.cobblemon.mod.common.util.cobblemonResource
+import com.cobblemon.mod.common.util.lang
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.resources.sounds.SimpleSoundInstance
+import net.minecraft.network.chat.Component
+
+class MoveSearchWidget(
+ private var pX: Int,
+ private var pY: Int,
+ text: Component = "MoveSearchWidget".text(),
+ update: () -> (Unit)
+): TextWidget(pX, pY, text = text, update = update) {
+
+ companion object {
+ const val ICON_SIZE = 16
+ const val SCALE = 0.5F
+
+ private val iconFilterResource = cobblemonResource("textures/gui/tmmachine/icon_search.png")
+ private val labelFilter = lang("ui.pokedex.search")
+ }
+
+ override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ blitk(
+ matrixStack = context.pose(),
+ x = (x - 10.5) / SCALE,
+ y = (y + 2.5) / SCALE,
+ texture = iconFilterResource,
+ width = ICON_SIZE,
+ height = ICON_SIZE,
+ vOffset = if (isFocused || !value.isEmpty() || iconHovered(mouseX, mouseY)) ICON_SIZE else 0,
+ textureHeight = ICON_SIZE * 2,
+ scale = SCALE
+ )
+
+ val input = applyTextCursor(value, labelFilter)
+ drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = input.bold(),
+ x = pX,
+ y = pY + 2,
+ shadow = true
+ )
+
+ renderCursor(context, input)
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ if (iconHovered(mouseX.toInt(), mouseY.toInt())) {
+ Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F, 0.25F))
+ // Simulate clicking on text area if icon is clicked
+ if (!isFocused) return super.mouseClicked(x + (width / 2.0), y + (height / 2.0), button)
+ }
+ return super.mouseClicked(mouseX, mouseY, button)
+ }
+
+ fun iconHovered(mouseX: Int, mouseY: Int): Boolean = mouseX >= (x - 10.5) && mouseY >= (y + 2.5)
+ && mouseX <= ((x - 10.5) + (ICON_SIZE * SCALE)) && mouseY <= ((y + 2.5) + (ICON_SIZE * SCALE))
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/MovesScrollingWidget.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/MovesScrollingWidget.kt
new file mode 100644
index 0000000000..371237731e
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/MovesScrollingWidget.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.gui.tmmachine
+
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.gui.blitk
+import com.cobblemon.mod.common.api.reactive.SettableObservable
+import com.cobblemon.mod.common.api.text.bold
+import com.cobblemon.mod.common.api.tms.TechnicalMachine
+import com.cobblemon.mod.common.client.CobblemonResources
+import com.cobblemon.mod.common.client.gui.MoveCategoryIcon
+import com.cobblemon.mod.common.client.gui.ScrollingWidget
+import com.cobblemon.mod.common.client.gui.TypeIcon
+import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.MoveSlotWidget
+import com.cobblemon.mod.common.client.gui.tmmachine.MovesScrollingWidget.ScrollSlot
+import com.cobblemon.mod.common.client.render.drawScaledText
+import com.cobblemon.mod.common.util.cobblemonResource
+import com.cobblemon.mod.common.util.lang
+import com.cobblemon.mod.common.util.math.toRGB
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.resources.sounds.SimpleSoundInstance
+import net.minecraft.network.chat.Component
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.util.FastColor
+import net.minecraft.util.Mth
+
+class MovesScrollingWidget(val pX: Int, val pY: Int, val tmList: SettableObservable<MutableList<TechnicalMachine>>, val setTM: (TechnicalMachine?, Boolean) -> (Unit)): ScrollingWidget<ScrollSlot>(
+ width = WIDTH,
+ height = HEIGHT,
+ left = pX,
+ top = pY - HEIGHT,
+ slotHeight = SLOT_HEIGHT + SLOT_SPACING
+) {
+ companion object {
+ const val WIDTH = 115
+ const val HEIGHT = 82
+ const val SLOT_HEIGHT = 22
+ const val SLOT_SPACING = 4
+
+ val searchBar = cobblemonResource("textures/gui/tmmachine/search_bar.png")
+ }
+
+ var highlightedMoveId: ResourceLocation? = null
+
+ init {
+ tmList.subscribeIncludingCurrent { tmList ->
+ replaceEntries(tmList.map { tm -> ScrollSlot(tm, setTM) })
+ }
+ }
+
+ fun setSlotHighlighted(id: ResourceLocation?) {
+ highlightedMoveId = id
+ for (child in children()) {
+ if (child is ScrollSlot) {
+ child.highlighted = child.tm.id == highlightedMoveId
+ }
+ }
+ }
+
+ override fun getScrollbarPosition(): Int = rowLeft + width - 3
+
+ override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ blitk(
+ matrixStack = context.pose(),
+ texture = searchBar,
+ x = left - 4,
+ y = y - 19,
+ width = 120,
+ height = 19
+ )
+ super.renderWidget(context, mouseX, mouseY, delta)
+ }
+
+ override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ val xLeft = this.scrollbarPosition
+ val xRight = xLeft + 3
+
+ val barHeight = this.bottom - this.y
+
+ var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
+ yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
+ var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
+ if (yTop < this.y) {
+ yTop = this.y
+ }
+
+ context.fill(xLeft, this.y, xRight, this.bottom, FastColor.ARGB32.color(255, 75, 75, 75)) // background
+ context.fill(xLeft,yTop, xRight, yTop + yBottom, FastColor.ARGB32.color(255, 141, 141, 141)) // base
+ }
+
+ override fun getEntry(index: Int): ScrollSlot = children()[index] as ScrollSlot
+
+ class ScrollSlot(val tm: TechnicalMachine, val setTM : (TechnicalMachine?, clicked: Boolean) -> Unit): Slot<ScrollSlot>() {
+ var posX: Int = 0
+ var posY: Int = 0
+ var highlighted = false
+
+ override fun render(context: GuiGraphics, index: Int, y: Int, x: Int, entryWidth: Int, entryHeight: Int, mouseX: Int, mouseY: Int, hovered: Boolean, tickDelta: Float) {
+ posX = x
+ posY = y
+ val moveTemplate = tm.moveName
+ val rgb = moveTemplate.elementalType.hue.toRGB()
+ val isHovered = hovered && (mouseX < x + (108)) // Prevent hover when over scrollbar
+ val startPosY = y + SLOT_SPACING
+ val startPosX = x + 1 // Hover border for move renders at x, so render move element to the right by 1
+
+ val matrices = context.pose()
+ blitk(
+ matrixStack = matrices,
+ texture = MoveSlotWidget.moveResource,
+ x = startPosX,
+ y = startPosY,
+ width = MoveSlotWidget.MOVE_WIDTH,
+ height = MoveSlotWidget.MOVE_HEIGHT,
+ vOffset = if (isHovered) MoveSlotWidget.MOVE_HEIGHT else 0,
+ textureHeight = MoveSlotWidget.MOVE_HEIGHT * 2,
+ red = rgb.first,
+ green = rgb.second,
+ blue = rgb.third
+ )
+
+ blitk(
+ matrixStack = matrices,
+ texture = MoveSlotWidget.moveOverlayResource,
+ x = startPosX,
+ y = startPosY,
+ width = MoveSlotWidget.MOVE_WIDTH,
+ height = MoveSlotWidget.MOVE_HEIGHT
+ )
+
+ if (highlighted || isHovered) {
+ blitk(
+ matrixStack = matrices,
+ texture = MoveSlotWidget.moveSelectedOverlayResource,
+ x = startPosX - 1,
+ y = startPosY - 1,
+ width = MoveSlotWidget.MOVE_WIDTH + 2,
+ height = MoveSlotWidget.MOVE_HEIGHT + 2
+ )
+ }
+
+ drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = lang("ui.moves.pp", moveTemplate.pp).bold(),
+ x = startPosX + 93,
+ y = startPosY + 13,
+ centered = true
+ )
+
+ // Type Icon
+ TypeIcon(
+ x = x + 2,
+ y = startPosY + 2,
+ type = moveTemplate.elementalType
+ ).render(context)
+
+ // Move Category
+ MoveCategoryIcon(
+ x = x + 66,
+ y = startPosY + 13.5,
+ category = moveTemplate.damageCategory
+ ).render(context)
+
+ // Move Name
+ drawScaledText(
+ context = context,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = moveTemplate.displayName.bold(),
+ x = x + 28,
+ y = startPosY + 2,
+ shadow = true
+ )
+
+ if (isHovered) {
+ setTM(tm, false)
+ }
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ // Prevent clicking through scroll bar
+ if (mouseX < posX + (108)) {
+ setTM(tm, true)
+ Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
+ }
+ return super.mouseClicked(mouseX, mouseY, button)
+ }
+
+ override fun getNarration(): Component {
+ return tm.moveName.displayName
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/StartButton.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/StartButton.kt
new file mode 100644
index 0000000000..dc513d458e
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/StartButton.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.gui.tmmachine
+
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.gui.blitk
+import com.cobblemon.mod.common.api.text.text
+import com.cobblemon.mod.common.client.gui.CobblemonRenderable
+import com.cobblemon.mod.common.util.cobblemonResource
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.components.Button
+import net.minecraft.client.gui.narration.NarrationElementOutput
+import net.minecraft.client.resources.sounds.SimpleSoundInstance
+import net.minecraft.client.sounds.SoundManager
+
+class StartButton(
+ var buttonX: Float,
+ var buttonY: Float,
+ val clickAction: OnPress
+): Button(buttonX.toInt(), buttonY.toInt(), WIDTH, HEIGHT, "".text(), clickAction, DEFAULT_NARRATION), CobblemonRenderable {
+ companion object {
+ const val WIDTH = 22
+ const val HEIGHT = 12
+
+ val base = cobblemonResource("textures/gui/tmmachine/button_start.png")
+ val baseStop = cobblemonResource("textures/gui/tmmachine/button_stop.png")
+ val baseDisabled = cobblemonResource("textures/gui/tmmachine/button_start_disabled.png")
+
+ val iconStart = cobblemonResource("textures/gui/tmmachine/button_icon_start.png")
+ val iconStop = cobblemonResource("textures/gui/tmmachine/button_icon_stop.png")
+ val iconRepeat = cobblemonResource("textures/gui/tmmachine/button_icon_repeat.png")
+ }
+
+ var disabled = false
+ var processing = false
+ var shouldRepeat = false
+
+ override fun mouseDragged(d: Double, e: Double, i: Int, f: Double, g: Double) = false
+ override fun defaultButtonNarrationText(builder: NarrationElementOutput) {}
+
+ override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) {
+ val resource = if (disabled) baseDisabled
+ else (if (processing) baseStop else base)
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = resource,
+ x = buttonX,
+ y = buttonY,
+ width = WIDTH,
+ height = HEIGHT,
+ vOffset = if (isMouseOver(mouseX.toDouble(), mouseY.toDouble())) HEIGHT else 0,
+ textureHeight = HEIGHT * 2,
+ )
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = if (processing) iconStop else (if (shouldRepeat) iconRepeat else iconStart),
+ x = buttonX,
+ y = buttonY,
+ width = WIDTH,
+ height = HEIGHT
+ )
+ }
+
+ /*override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ if (active && visible && !disabled && isMouseOver(mouseX, mouseY)) {
+ super.mouseClicked(mouseX, mouseY, button)
+ }
+ return false
+ }*/
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ if (!active || !visible || disabled) return false
+ return super.mouseClicked(mouseX, mouseY, button)
+ }
+
+ override fun playDownSound(soundManager: SoundManager) {
+ if (visible && !disabled) {
+ soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/TMMachineScreen.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/TMMachineScreen.kt
new file mode 100644
index 0000000000..639cc68de7
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/TMMachineScreen.kt
@@ -0,0 +1,1040 @@
+/*
+ * 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.gui.tmmachine
+
+import com.cobblemon.mod.common.CobblemonItems
+import com.cobblemon.mod.common.CobblemonNetwork
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.gui.ColourLibrary
+import com.cobblemon.mod.common.api.gui.MultiLineLabelK
+import com.cobblemon.mod.common.api.gui.blitk
+import com.cobblemon.mod.common.api.moves.MoveTemplate
+import com.cobblemon.mod.common.api.moves.Moves
+import com.cobblemon.mod.common.api.reactive.SettableObservable
+import com.cobblemon.mod.common.api.text.bold
+import com.cobblemon.mod.common.api.text.text
+import com.cobblemon.mod.common.api.tms.TechnicalMachine
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.api.types.ElementalType
+import com.cobblemon.mod.common.api.types.ElementalTypes
+import com.cobblemon.mod.common.block.entity.TMMachineBlockEntity
+import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
+import com.cobblemon.mod.common.client.CobblemonClient
+import com.cobblemon.mod.common.client.CobblemonResources
+import com.cobblemon.mod.common.client.gui.interact.moveselect.MoveSlotButton
+import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.MovesWidget
+import com.cobblemon.mod.common.client.settings.ServerSettings
+import com.cobblemon.mod.common.client.render.drawScaledText
+import com.cobblemon.mod.common.client.render.drawScaledTextJustifiedRight
+import com.cobblemon.mod.common.item.components.TMMoveComponent
+import com.cobblemon.mod.common.net.messages.client.ui.SetActiveTMPacket
+import com.cobblemon.mod.common.net.messages.client.ui.SetTMMachineContainerDataPacket
+import com.cobblemon.mod.common.net.messages.server.block.TMMachineTeachMovePacket
+import com.cobblemon.mod.common.pokemon.Pokemon
+import com.cobblemon.mod.common.util.asTranslated
+import com.cobblemon.mod.common.util.cobblemonResource
+import com.cobblemon.mod.common.util.isInventoryKeyPressed
+import com.cobblemon.mod.common.util.lang
+import com.cobblemon.mod.common.util.math.toRGB
+import com.mojang.blaze3d.platform.InputConstants
+import com.mojang.blaze3d.vertex.PoseStack
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.client.resources.sounds.SimpleSoundInstance
+import net.minecraft.core.BlockPos
+import net.minecraft.core.registries.BuiltInRegistries
+import net.minecraft.network.chat.Component
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.sounds.SoundEvent
+import net.minecraft.world.entity.player.Inventory
+import net.minecraft.world.inventory.ClickType
+import net.minecraft.world.inventory.Slot
+import net.minecraft.world.item.ItemStack
+import kotlin.math.ceil
+
+class TMMachineScreen(containerMenu: TMMachineMenu, val inventory: Inventory, title: Component) : AbstractContainerScreen<TMMachineMenu>(containerMenu, inventory, title) {
+ companion object {
+ const val HEIGHT = 226
+ const val WIDTH = 191
+ const val HALF_SCALE = 0.5F
+
+ const val DISC_DIAMETER = 56
+
+ const val LOGO_WIDTH = 86
+ const val LOGO_HEIGHT = 64
+
+ const val TYPE_SELECT_MODE = 1
+ const val MOVE_SELECT_MODE = 2
+ const val TM_BURN_MODE = 3
+
+ const val CRAFT_TICKS = 10 // Length for craft completion animation
+ const val RESET_DISC_TICKS = 4 // Length for disc reset animation
+
+ const val SCREEN_SAVER_TIMEOUT_TICKS = 200 // 10 Seconds
+ const val SCREEN_SAVER_WIDTH = 118
+ const val SCREEN_SAVER_HEIGHT = 110
+
+ val baseResource = cobblemonResource("textures/gui/tmmachine/base.png")
+ val tmTray = cobblemonResource("textures/gui/tmmachine/tm_tray.png")
+ val iconArrowLeft = cobblemonResource("textures/gui/tmmachine/icon_arrow_left.png")
+ val iconArrowRight = cobblemonResource("textures/gui/tmmachine/icon_arrow_right.png")
+ val emptyDiscSlot = cobblemonResource("textures/item/tms/blank_disc_empty_slot.png")
+
+ val discBase = cobblemonResource("textures/gui/tmmachine/tm_base.png")
+ val discBorder = cobblemonResource("textures/gui/tmmachine/tm_border.png")
+ val discOverlay = cobblemonResource("textures/gui/tmmachine/tm_base_overlay.png")
+ val discReflective = cobblemonResource("textures/gui/tmmachine/tm_overlay_reflective.png")
+ val discReflectiveRotating = cobblemonResource("textures/gui/tmmachine/tm_overlay_reflective_rotating.png")
+ val discRotating = cobblemonResource("textures/gui/tmmachine/tm_overlay_rotating.png")
+
+ val logo = cobblemonResource("textures/gui/tmmachine/logo_tm.png")
+ val screenSaver = cobblemonResource("textures/gui/tmmachine/screen_saver_background.png")
+ val screenOverlay = cobblemonResource("textures/gui/tmmachine/screen_overlay.png")
+ val scanLines = cobblemonResource("textures/gui/tmmachine/scan_lines.png")
+
+ val logoResourceMap: Map<ResourceLocation, List<String>> = mapOf(
+ cobblemonResource("textures/gui/tmmachine/logo_ct.png") to listOf("fr"),
+ cobblemonResource("textures/gui/tmmachine/logo_mt.png") to listOf("es", "it", "pt")
+ )
+
+ fun getLogoResource(langCode: String?): ResourceLocation =
+ logoResourceMap.entries.find { it.value.contains(langCode?.substringBefore("_")) }?.key ?: logo
+ }
+
+ var selectedTM: TechnicalMachine? = null
+ var heldStackMove: MoveTemplate? = null
+ var tmList: SettableObservable<MutableList<TechnicalMachine>> = SettableObservable(mutableListOf())
+ var output: ItemStack? = null
+ var mode: Int = TYPE_SELECT_MODE
+ var sortType: ElementalType? = null
+ var selectedPokemon: Pokemon? = null
+
+ var partySlotList: MutableSet<TMPartySlotWidget> = mutableSetOf()
+
+ lateinit var backButton: IconButton
+ lateinit var startButton: StartButton
+ lateinit var selectedMoveButton: MoveSlotButton
+ lateinit var typesScrollList: TypesScrollingWidget
+ lateinit var movesScrollingList: MovesScrollingWidget
+ lateinit var moveSearchWidget: MoveSearchWidget
+
+ var discReflectionFrame: Int = 0
+ var discRotationFrame: Int = 0
+
+ var ticksElapsed: Int = 0
+
+ var logoResource: ResourceLocation = logo
+ var logoTint: Triple<Double, Double, Double> = ElementalTypes.all().random().secondaryColor.toRGB()
+ var logoPosX: Int = 0
+ var logoPosY: Int = 0
+ var logoSlideRight: Boolean = true
+ var logoSlideDown: Boolean = true
+ var scanPosY: Int = 0
+ var scanLineOffsetY: Double = 0.0
+
+ var initScreen: Boolean = false
+
+ override fun init() {
+ imageWidth = WIDTH
+ imageHeight = HEIGHT
+
+ super.init()
+
+ logoResource = getLogoResource(minecraft?.languageManager?.selected)
+
+ logoPosX = leftPos + 1
+ logoPosY = topPos + 1
+ scanPosY = topPos + 1
+
+ partySlotList.clear()
+ CobblemonClient.storage.party.forEachIndexed { index, pokemon ->
+ partySlotList.add(TMPartySlotWidget(
+ pX = leftPos - 60,
+ pY = topPos + 9 + ((TMPartySlotWidget.HEIGHT + 3) * index),
+ pokemon = pokemon,
+ onPress = {
+ if ((it as TMPartySlotWidget).clickable) {
+ if (heldStackMove != null && pokemon != null) {
+ heldStackMove?.let { move ->
+ if (canLearnTMMove(move, pokemon) == TMPartySlotWidget.CAN_LEARN) {
+ CobblemonNetwork.sendToServer(TMMachineTeachMovePacket(pokemon.uuid, menu.carried, move))
+ }
+ }
+ } else {
+ if (mode == TYPE_SELECT_MODE && pokemon != null) {
+ setMoveList(pokemon = pokemon)
+ setScreenFromMode(MOVE_SELECT_MODE)
+ selectedPokemon = pokemon
+ playSound(CobblemonSounds.GUI_CLICK)
+ }
+ }
+ }
+ }
+ ))
+ }
+
+ for (slot in partySlotList) {
+ if (!children().contains(slot)) addRenderableWidget(slot)
+ }
+
+ typesScrollList = TypesScrollingWidget(
+ pX = leftPos + 7,
+ pY = topPos + 11,
+ setType = { type ->
+ sortType = type
+ setMoveList()
+ setScreenFromMode(MOVE_SELECT_MODE)
+ }
+ )
+ if (!children().contains(typesScrollList)) addRenderableWidget(typesScrollList)
+
+ movesScrollingList = MovesScrollingWidget(
+ pX = leftPos + 4,
+ pY = topPos + 29,
+ tmList,
+ setTM = { tm, clicked -> setSelectedTM(tm, clicked) }
+ )
+ if (!children().contains(movesScrollingList)) addRenderableWidget(movesScrollingList)
+
+ moveSearchWidget = MoveSearchWidget(pX = leftPos + 16, pY = topPos + 13) {
+ if (::movesScrollingList.isInitialized && moveSearchWidget.isFocused) {
+ val input = moveSearchWidget.value
+ setMoveList(filter = if (input.isBlank()) null else input)
+ }
+ }
+ if (!children().contains(moveSearchWidget)) addRenderableWidget(moveSearchWidget)
+
+ backButton = IconButton(
+ leftPos + 90F,
+ topPos + 1F,
+ 18,
+ 18,
+ iconArrowLeft,
+ hoverExtendRight = 11
+ ) { setScreenFromMode(TYPE_SELECT_MODE) }
+ if (!children().contains(backButton)) addRenderableWidget(backButton)
+
+ selectedMoveButton = MoveSlotButton(
+ leftPos + 6,
+ topPos + 6,
+ move = selectedTM?.moveName,
+ pp = selectedTM?.moveName?.pp ?: 0,
+ ppAsFraction = false
+ ) {
+ if (!isBurnActive()) {
+ if (::movesScrollingList.isInitialized && movesScrollingList.children().isEmpty()) setMoveList()
+ playSound(CobblemonSounds.GUI_CLICK)
+ setScreenFromMode(MOVE_SELECT_MODE)
+
+ // Clear active TM
+ CobblemonNetwork.sendToServer(SetActiveTMPacket(null))
+ }
+ }
+ if (!children().contains(selectedMoveButton)) addRenderableWidget(selectedMoveButton)
+
+ startButton = StartButton(leftPos + 5F, topPos + 35F) {
+ if (!(it as StartButton).disabled) {
+ selectedTM?.let {
+ val burnActive = menu.containerData?.get(TMMachineBlockEntity.BURN_ACTIVE_INDEX) ?: 0
+ CobblemonNetwork.sendToServer(SetTMMachineContainerDataPacket(
+ TMMachineBlockEntity.BURN_ACTIVE_INDEX,
+ burnActive xor 1 // Toggle active state, 1 Γçå 0
+ ))
+
+ /*// If button is not active, set if process should repeat or not
+ if (burnActive == 0) {
+ CobblemonNetwork.sendToServer(SetTMMachineContainerDataPacket(
+ TMMachineBlockEntity.REPEAT_PROCESS_INDEX,
+ if (hasShiftDown()) 1 else 0
+ ))
+ }*/
+
+ CobblemonNetwork.sendToServer(SetActiveTMPacket(it))
+ inventory.setChanged()
+ menu.broadcastChanges()
+ }
+ }
+ }
+ if (!children().contains(startButton)) addRenderableWidget(startButton)
+
+ setScreenFromMode(mode)
+ initScreen = false
+ }
+
+ override fun containerTick() {
+ ticksElapsed++
+ scanPosY++
+ scanLineOffsetY = (scanLineOffsetY + 0.5) % 6
+ if (scanPosY + 1 >= (topPos + SCREEN_SAVER_HEIGHT) * 2) scanPosY = 0
+
+ if (mode == TM_BURN_MODE) {
+ selectedTM?.let { tm ->
+ val resultStack = menu.inventory!!.getItem(TMMachineMenu.RESULT_SLOT)
+ val craftedTmStack = ItemStack(CobblemonItems.TECHNICAL_MACHINE)
+ .also { TMMoveComponent.setTMMove(it, tm.moveName) }
+
+ var validOutput = resultStack.isEmpty || (
+ !resultStack.isEmpty
+ && (resultStack.count < resultStack.maxStackSize)
+ && ItemStack.isSameItemSameComponents(resultStack, craftedTmStack)
+ )
+ var validCost = !menu.inventory!!.getItem(TMMachineMenu.BLANK_TM_SLOT).isEmpty // Blank TM slot
+
+ if (validOutput && validCost) {
+ val recipes = tm.getClampedRecipe() ?: listOf()
+ recipes.forEachIndexed { index, recipe ->
+ val slotStack = menu.inventory!!.getItem(index + TMMachineMenu.INGREDIENT_SLOTS.first)
+ if (!recipe.ingredient.test(slotStack) || slotStack.count < recipe.count) {
+ validCost = false
+ return@forEachIndexed
+ }
+ }
+ }
+
+ if (::startButton.isInitialized) {
+ val discResetting = menu.getPostCraftTicks() > CRAFT_TICKS
+ val burning = isBurnActive()
+
+ startButton.disabled = !burning && (discResetting || !(validCost && validOutput))
+ startButton.processing = burning
+ startButton.shouldRepeat = false
+ }
+
+ if (::selectedMoveButton.isInitialized) {
+ selectedMoveButton.enabled = !isBurnActive()
+ }
+
+ if (isBurnActive()) {
+ val progress = menu.getBurnProgressRatio()
+ if (progress > 0 && progress < 1) {
+ discReflectionFrame = (discReflectionFrame + 1) % 4
+ discRotationFrame = (discRotationFrame + 1) % 8
+ }
+ if ((progress == 1F) && (menu.getPostCraftTicks() == 0)) playSound(CobblemonSounds.TM_MACHINE_CRAFT)
+ }
+ }
+ } else {
+ // Check if screen should be switched upon opening UI
+ if (!initScreen) loadBurnScreenData()
+ }
+
+ val heldStack = menu.carried
+ if (heldStack.item == CobblemonItems.TECHNICAL_MACHINE) {
+ // Store move of TM item picked up by player cursor.
+ TMMoveComponent.getTMMove(heldStack)?.let { move ->
+ if (heldStackMove != move) heldStackMove = move
+ if (mode == TYPE_SELECT_MODE) updatePartySlotStatus(heldStackMove)
+ }
+ } else if (heldStackMove != null) {
+ heldStackMove = null
+ updatePartySlotStatus(null)
+ }
+
+ // Allow dragging held TMs onto party slots in both move list and burn views.
+ when (mode) {
+ TM_BURN_MODE -> {
+ for (slot in partySlotList) {
+ slot.clickable = heldStackMove != null && (selectedTM == null || heldStackMove == selectedTM?.moveName)
+ }
+ updatePartySlotStatus(selectedTM?.moveName ?: heldStackMove)
+ }
+ MOVE_SELECT_MODE -> {
+ for (slot in partySlotList) slot.clickable = heldStackMove != null
+ updatePartySlotStatus(heldStackMove)
+ }
+ }
+
+ // Screen saver
+ if (ticksElapsed > SCREEN_SAVER_TIMEOUT_TICKS) {
+ logoPosX = logoPosX + (if (logoSlideRight) 1 else -1)
+ logoPosY = logoPosY + (if (logoSlideDown) 1 else -1)
+ var borderHit = false
+
+ if (logoSlideRight && (logoPosX + (LOGO_WIDTH * HALF_SCALE).toInt()) >= (leftPos + SCREEN_SAVER_WIDTH + 1)) {
+ logoSlideRight = false
+ borderHit = true
+ }
+ if ((!logoSlideRight) && logoPosX <= (leftPos + 1)) {
+ logoSlideRight = true
+ borderHit = true
+ }
+
+ if (logoSlideDown && (logoPosY + (LOGO_HEIGHT * HALF_SCALE).toInt()) >= (topPos + SCREEN_SAVER_HEIGHT + 1)) {
+ logoSlideDown = false
+ borderHit = true
+ }
+ if ((!logoSlideDown) && logoPosY <= (topPos + 1)) {
+ logoSlideDown = true
+ borderHit = true
+ }
+
+ if (borderHit) {
+ logoTint = ElementalTypes.all().random().secondaryColor.toRGB()
+ }
+ }
+
+ super.containerTick()
+ }
+
+ private fun resetScreenSaver(): Boolean {
+ if (ticksElapsed >= SCREEN_SAVER_TIMEOUT_TICKS) {
+ ticksElapsed = 0
+ return true
+ }
+ return false
+ }
+
+ private fun loadBurnScreenData() {
+ var tm: TechnicalMachine? = null
+
+ val blockPos = menu.containerData?.let {
+ val x = it.get(TMMachineBlockEntity.POSITION_X_INDEX)
+ val y = it.get(TMMachineBlockEntity.POSITION_Y_INDEX)
+ val z = it.get(TMMachineBlockEntity.POSITION_Z_INDEX)
+ BlockPos(x, y, z)
+ }
+
+ inventory.player.level().getBlockEntity(blockPos)?.let { blockEntity ->
+ if (blockEntity is TMMachineBlockEntity) {
+ Moves.getByName(blockEntity.activeMove)?.let {
+ tm = TechnicalMachines.moveToTM[it]
+ }
+ }
+ }
+
+ // If burn currently in progress or if active TM set, set to burn screen
+ if (isBurnActive() || tm != null) {
+ setSelectedTM(tm, true)
+ }
+
+ initScreen = true
+ }
+
+ private fun isBurnActive(): Boolean = (menu.containerData?.get(TMMachineBlockEntity.BURN_ACTIVE_INDEX) ?: 0) == 1
+
+ private fun setScreenFromMode(screen: Int) {
+ mode = screen
+ when (mode) {
+ TYPE_SELECT_MODE -> {
+ toggleTMBurn(false)
+ toggleMoveSelect(false)
+ toggleTypeSelect(true)
+ }
+ MOVE_SELECT_MODE -> {
+ toggleTMBurn(false)
+ toggleTypeSelect(false)
+ toggleMoveSelect(true)
+ }
+ else -> {
+ toggleTypeSelect(false)
+ toggleMoveSelect(false)
+ toggleTMBurn(true)
+ }
+ }
+ }
+
+ private fun setMoveList(filter: String? = null, pokemon: Pokemon? = null) {
+ val filteredList = TechnicalMachine.filterTms(
+ search = filter,
+ type = sortType,
+ pokemon = pokemon,
+ player = inventory.player,
+ includeUnlearned = ServerSettings.unlockAllMoveDexMovesByDefault
+ ).toMutableList()
+ if (sortType == null) filteredList.sortBy { it.type }
+ tmList.set(filteredList)
+ }
+
+ private fun toggleTypeSelect(isVisible: Boolean) {
+ if (isVisible) {
+ setSelectedTM(null, false)
+ if (::moveSearchWidget.isInitialized) moveSearchWidget.value = ""
+ if (selectedPokemon != null) selectedPokemon = null
+ sortType = null
+ }
+
+ if (::typesScrollList.isInitialized) {
+ typesScrollList.apply {
+ visible = isVisible
+ scrollAmount = 0.0
+ // Resize widget to toggle input detection as screen only detects first widget added for overlapping children
+ if (isVisible) { setSize(TypesScrollingWidget.WIDTH, TypesScrollingWidget.HEIGHT) }
+ else { setSize(0, 0) }
+ }
+ }
+
+ for (slot in partySlotList) slot.clickable = isVisible
+ }
+
+ private fun toggleMoveSelect(isVisible: Boolean) {
+ if (::movesScrollingList.isInitialized) {
+ movesScrollingList.apply {
+ visible = isVisible
+ scrollAmount = 0.0
+ if (isVisible) { setSize(MovesScrollingWidget.WIDTH, MovesScrollingWidget.HEIGHT) }
+ else { setSize(0, 0) }
+ }
+ }
+ if (::moveSearchWidget.isInitialized) {
+ moveSearchWidget.visible = isVisible
+ }
+ if (::backButton.isInitialized) {
+ backButton.visible = isVisible
+ }
+ heldStackMove = null
+ }
+
+ private fun toggleTMBurn(isVisible: Boolean) {
+ if (!isVisible) setSelectedTM(null, false)
+ if (::selectedMoveButton.isInitialized) selectedMoveButton.visible = isVisible
+ if (::startButton.isInitialized) startButton.visible = isVisible
+ }
+
+ fun canLearnTMMove(move: MoveTemplate, pokemon: Pokemon): Int {
+ if (pokemon.moveSet.getMoveTemplates().contains(move) || pokemon.allAccessibleMoves.contains(move)) return TMPartySlotWidget.LEARNED
+ val learnableMoves = pokemon.form.moves.tmLearnableMoves()
+ return if (learnableMoves.contains(move)) TMPartySlotWidget.CAN_LEARN else TMPartySlotWidget.CANNOT_LEARN
+ }
+
+ fun setSelectedTM(tm: TechnicalMachine?, clicked: Boolean) {
+ if (selectedTM != tm) {
+ selectedTM = tm
+ updatePartySlotStatus(tm?.moveName)
+ }
+
+ if (::movesScrollingList.isInitialized) {
+ movesScrollingList.setSlotHighlighted(if (clicked) null else selectedTM?.id)
+ }
+
+ if (clicked) {
+ CobblemonNetwork.sendToServer(SetActiveTMPacket(tm))
+
+ selectedTM?.let {
+ if (::selectedMoveButton.isInitialized) {
+ selectedMoveButton.apply {
+ move = it.moveName
+ pp = it.moveName.pp
+ }
+ setScreenFromMode(TM_BURN_MODE)
+ }
+ }
+ }
+ }
+
+ fun updatePartySlotStatus(moveTemplate: MoveTemplate?) {
+ for (slot in partySlotList) {
+ if (slot.pokemon != null) {
+ slot.teachable = if (moveTemplate != null) canLearnTMMove(moveTemplate, slot.pokemon) else null
+ }
+ }
+ }
+
+ fun renderDiscTypeOverlay(poseStack: PoseStack, isDiscRotating: Boolean, alpha: Float = 1F) {
+ selectedTM?.let {
+ val primaryRgb = it.moveName.elementalType.primaryColor.toRGB()
+ val secondaryRgb = it.moveName.elementalType.secondaryColor.toRGB()
+ blitk(
+ matrixStack = poseStack,
+ texture = discBase,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ red = primaryRgb.first,
+ green = primaryRgb.second,
+ blue = primaryRgb.third,
+ alpha = alpha
+ )
+
+ if (isDiscRotating) {
+ // Reflective rotation
+ blitk(
+ matrixStack = poseStack,
+ texture = discReflectiveRotating,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ vOffset = DISC_DIAMETER * discReflectionFrame,
+ textureHeight = DISC_DIAMETER * 4,
+ red = secondaryRgb.first,
+ green = secondaryRgb.second,
+ blue = secondaryRgb.third,
+ alpha = alpha
+ )
+
+ // Groove rotation
+ blitk(
+ matrixStack = poseStack,
+ texture = discRotating,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ vOffset = DISC_DIAMETER * discRotationFrame,
+ textureHeight = DISC_DIAMETER * 8,
+ red = secondaryRgb.first,
+ green = secondaryRgb.second,
+ blue = secondaryRgb.third,
+ alpha = alpha
+ )
+ } else {
+ blitk(
+ matrixStack = poseStack,
+ texture = discReflective,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ red = secondaryRgb.first,
+ green = secondaryRgb.second,
+ blue = secondaryRgb.third,
+ alpha = alpha
+ )
+ }
+
+ val postCraftTicks = menu.getPostCraftTicks()
+ if (postCraftTicks > 0 && postCraftTicks < 8) {
+ val opacity = when {
+ postCraftTicks <= 5 -> postCraftTicks * 0.25F
+ postCraftTicks > 5 -> (CRAFT_TICKS - postCraftTicks) * 0.25F // decreases by 0.25 each tick after 5
+ else -> 0F
+ }
+
+ blitk(
+ matrixStack = poseStack,
+ texture = discOverlay,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ alpha = opacity
+ )
+ }
+ }
+ }
+
+ fun renderDisc(poseStack: PoseStack, isDiscRotating: Boolean, baseAlpha: Float = 1F, overlayAlpha: Float = 1F) {
+ blitk(
+ matrixStack = poseStack,
+ texture = discBase,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ alpha = baseAlpha
+ )
+
+ if (isDiscRotating) {
+ // Reflective rotation
+ blitk(
+ matrixStack = poseStack,
+ texture = discReflectiveRotating,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ vOffset = DISC_DIAMETER * discReflectionFrame,
+ textureHeight = DISC_DIAMETER * 4,
+ alpha = baseAlpha
+ )
+
+ // Groove rotation
+ blitk(
+ matrixStack = poseStack,
+ texture = discRotating,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ vOffset = DISC_DIAMETER * discRotationFrame,
+ textureHeight = DISC_DIAMETER * 8,
+ alpha = baseAlpha
+ )
+ } else {
+ blitk(
+ matrixStack = poseStack,
+ texture = discReflective,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ alpha = baseAlpha
+ )
+ }
+
+ if (menu.getPostCraftTicks() < CRAFT_TICKS) {
+ renderDiscTypeOverlay(poseStack, isDiscRotating, overlayAlpha)
+ }
+
+ blitk(
+ matrixStack = poseStack,
+ texture = discBorder,
+ x = leftPos + 32,
+ y = topPos + 50,
+ width = DISC_DIAMETER,
+ height = DISC_DIAMETER,
+ alpha = baseAlpha
+ )
+ }
+
+ fun renderMoveInfo(context: GuiGraphics, mouseX: Int, mouseY: Int) {
+ val currentTm = selectedTM
+ val moveTemplate = currentTm?.moveName
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = MovesWidget.movesPowerIconResource,
+ x = leftPos + 127.5,
+ y = topPos + 7.5,
+ width = 5,
+ height = 5
+ )
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = MovesWidget.movesAccuracyIconResource,
+ x = leftPos + 127.5,
+ y = topPos + 19.5,
+ width = 5,
+ height = 5
+ )
+
+ blitk(
+ matrixStack = context.pose(),
+ texture = MovesWidget.movesEffectIconResource,
+ x = leftPos + 127.5,
+ y = topPos + 31.5,
+ width = 5,
+ height = 5
+ )
+
+ drawScaledText(
+ context = context,
+ text = lang("ui.power"),
+ x = leftPos + 134.5,
+ y = topPos + 8,
+ shadow = true,
+ scale = HALF_SCALE
+ )
+
+ drawScaledText(
+ context = context,
+ text = lang("ui.accuracy"),
+ x = leftPos + 134.5,
+ y = topPos + 20,
+ shadow = true,
+ scale = HALF_SCALE
+ )
+
+ drawScaledText(
+ context = context,
+ text = lang("ui.effect"),
+ x = leftPos + 134.5,
+ y = topPos + 32,
+ shadow = true,
+ scale = HALF_SCALE
+ )
+
+ val movePower = if (moveTemplate != null && moveTemplate.power.toInt() > 0) moveTemplate.power.toInt().toString().text() else "ΓÇö".text()
+ drawScaledTextJustifiedRight(
+ context = context,
+ text = movePower,
+ x = leftPos + 183,
+ y = topPos + 8,
+ shadow = true,
+ scale = HALF_SCALE
+ )
+
+ val moveAccuracy = if (moveTemplate != null) MovesWidget.format(moveTemplate.accuracy).text() else "ΓÇö".text()
+ drawScaledTextJustifiedRight(
+ context = context,
+ text = moveAccuracy,
+ x = leftPos + 183,
+ y = topPos + 20,
+ shadow = true,
+ scale = HALF_SCALE
+ )
+
+ val moveEffect = if (moveTemplate != null) MovesWidget.format(moveTemplate.effectChances.firstOrNull() ?: 0.0).text() else "ΓÇö".text()
+ drawScaledTextJustifiedRight(
+ context = context,
+ text = moveEffect,
+ x = leftPos + 183,
+ y = topPos + 32,
+ shadow = true,
+ scale = HALF_SCALE
+ )
+
+ if (moveTemplate != null) {
+ context.pose().pushPose()
+ context.pose().scale(HALF_SCALE, HALF_SCALE, 1F)
+ MultiLineLabelK.create(
+ component = moveTemplate.description,
+ width = 55 / HALF_SCALE,
+ maxLines = 5
+ ).renderLeftAligned(
+ context = context,
+ x = (leftPos + 127.5) / HALF_SCALE,
+ y = (topPos + 44.5) / HALF_SCALE,
+ ySpacing = 6 / HALF_SCALE,
+ colour = ColourLibrary.WHITE,
+ shadow = true
+ )
+ context.pose().popPose()
+
+ val recipe = selectedTM?.getClampedRecipe() ?: listOf()
+ // Render material cost
+ recipe.forEachIndexed { index, recipe ->
+ val itemX = (leftPos + 129 + (index * 18))
+ val itemY = (topPos + 90)
+
+ val stacks = recipe.ingredient.items
+ if (stacks.isEmpty()) return@forEachIndexed
+
+ val level = Minecraft.getInstance().level ?: return@forEachIndexed
+ val index = ((level.gameTime / 20) % stacks.size).toInt()
+
+ val itemStack = stacks[index].copy().also { it.count = recipe.count }
+
+ context.renderItem(itemStack, itemX, itemY)
+ context.renderItemDecorations(Minecraft.getInstance().font, itemStack, itemX, itemY)
+
+ if (
+ mouseX >= itemX && mouseX < itemX + 16 &&
+ mouseY >= itemY && mouseY < itemY + 16
+ ) {
+ context.renderTooltip(Minecraft.getInstance().font, itemStack, mouseX, mouseY)
+ }
+ }
+ }
+ }
+
+ fun renderScreenSaver(context: GuiGraphics) {
+ val poseStack = context.pose()
+ // Screen saver
+ if (ticksElapsed > SCREEN_SAVER_TIMEOUT_TICKS) {
+ val opacity = Math.max(0.0, Math.min(1.0, (ticksElapsed - SCREEN_SAVER_TIMEOUT_TICKS) * 0.2))
+ poseStack.pushPose()
+ poseStack.translate(0.0, 0.0, 100.0)
+ blitk(
+ matrixStack = poseStack,
+ texture = screenSaver,
+ x = leftPos + 1,
+ y = topPos + 1,
+ width = SCREEN_SAVER_WIDTH,
+ height = SCREEN_SAVER_HEIGHT,
+ alpha = opacity
+ )
+
+ blitk(
+ matrixStack = poseStack,
+ texture = logoResource,
+ x = logoPosX / HALF_SCALE,
+ y = logoPosY / HALF_SCALE,
+ width = LOGO_WIDTH,
+ height = LOGO_HEIGHT,
+ scale = HALF_SCALE,
+ red = logoTint.first,
+ green = logoTint.second,
+ blue = logoTint.third,
+ alpha = 0.8 * opacity
+ )
+
+ blitk(
+ matrixStack = poseStack,
+ texture = cobblemonResource("textures/white.png"),
+ x = leftPos + 1,
+ y = scanPosY * 0.5,
+ width = SCREEN_SAVER_WIDTH,
+ height = 1,
+ red = 0,
+ green = 0,
+ blue = 0,
+ alpha = 0.1 * opacity
+ )
+
+ context.enableScissor(
+ leftPos + 1,
+ topPos + 1,
+ leftPos + 1 + SCREEN_SAVER_WIDTH,
+ topPos + 1 + SCREEN_SAVER_HEIGHT
+ )
+ blitk(
+ matrixStack = poseStack,
+ texture = scanLines,
+ x = leftPos + 1,
+ y = topPos + 1 - scanLineOffsetY,
+ width = 118,
+ height = 114,
+ alpha = opacity
+ )
+ context.disableScissor()
+
+ blitk(
+ matrixStack = poseStack,
+ texture = screenOverlay,
+ x = leftPos + 1,
+ y = topPos + 1,
+ width = SCREEN_SAVER_WIDTH,
+ height = SCREEN_SAVER_HEIGHT,
+ alpha = 0.5 * opacity
+ )
+
+ poseStack.popPose()
+ }
+ }
+
+ override fun mouseMoved(mouseX: Double, mouseY: Double) {
+ ticksElapsed = 0
+ super.mouseMoved(mouseX, mouseY)
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ if (resetScreenSaver()) return false
+ return super.mouseClicked(mouseX, mouseY, button)
+ }
+
+ override fun slotClicked(slot: Slot?, slotId: Int, mouseButton: Int, type: ClickType) {
+ super.slotClicked(slot, slotId, mouseButton, type)
+ if (slot != null && slotId in 0 until Inventory.INVENTORY_SIZE && selectedTM != null) {
+ setSelectedTM(null, false)
+ }
+ }
+
+ override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
+ if (resetScreenSaver()) return false
+ return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY)
+ }
+
+ override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ if (resetScreenSaver()) return false
+ val searchFocused = this::moveSearchWidget.isInitialized && moveSearchWidget.isFocused
+
+ if (isInventoryKeyPressed(minecraft, keyCode, scanCode)) {
+ if (searchFocused) { return true }
+ else {
+ playSound(CobblemonSounds.TM_MACHINE_OFF)
+ Minecraft.getInstance().setScreen(null)
+ return true
+ }
+ }
+
+ if (keyCode == InputConstants.KEY_ESCAPE) {
+ if (searchFocused) {
+ // Escape from text box
+ this.focused = null
+ return true
+ } else {
+ playSound(CobblemonSounds.TM_MACHINE_OFF)
+ Minecraft.getInstance().setScreen(null)
+ return true
+ }
+ }
+
+ return super.keyPressed(keyCode, scanCode, modifiers)
+ }
+
+ override fun renderBackground(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {}
+
+ override fun renderBg(guiGraphics: GuiGraphics, partialTick: Float, mouseX: Int, mouseY: Int) {}
+
+ override fun render(graphics: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ super.renderTransparentBackground(graphics)
+
+ val matrices = graphics.pose()
+ blitk(
+ matrixStack = matrices,
+ texture = baseResource,
+ x = leftPos,
+ y = topPos,
+ width = WIDTH,
+ height = HEIGHT
+ )
+
+ blitk(
+ matrixStack = matrices,
+ texture = emptyDiscSlot,
+ x = leftPos + 105,
+ y = topPos + 116,
+ width = 16,
+ height = 16
+ )
+
+ var label = if (mode != MOVE_SELECT_MODE) "block.cobblemon.tm_machine".asTranslated()
+ else ( if (selectedPokemon != null) selectedPokemon!!.getDisplayName() else (if (sortType != null) lang("type.suffix", sortType!!.displayName) else lang("ui.moves")))
+ drawScaledText(
+ context = graphics,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = label.bold(),
+ x = leftPos + 5,
+ y = topPos + 1
+ )
+
+ if (mode == MOVE_SELECT_MODE && ::backButton.isInitialized) {
+ drawScaledText(
+ context = graphics,
+ font = CobblemonResources.DEFAULT_LARGE,
+ text = lang("ui.back").bold(),
+ x = backButton.x + backButton.width + - 8.0,
+ y = backButton.y,
+ shadow = true
+ )
+ }
+
+ if (mode == TM_BURN_MODE) {
+ blitk(
+ matrixStack = matrices,
+ texture = tmTray,
+ x = leftPos,
+ y = topPos,
+ width = 120,
+ height = 112
+ )
+
+ val burnProgressWidth = ceil(menu.getBurnProgressRatio() * 81).toInt()
+ blitk(
+ matrixStack = matrices,
+ texture = CobblemonResources.WHITE,
+ x = leftPos + 33,
+ y = topPos + 36,
+ width = burnProgressWidth,
+ height = 2
+ )
+
+ if (!menu.inventory!!.getItem(TMMachineMenu.BLANK_TM_SLOT).isEmpty) {
+ val postCraftTicks = menu.getPostCraftTicks()
+ val baseOpacity = when {
+ ((postCraftTicks >= (CRAFT_TICKS + 1)) && (postCraftTicks <= (CRAFT_TICKS + RESET_DISC_TICKS))) -> (postCraftTicks - (CRAFT_TICKS + 1)) * 0.25F
+ postCraftTicks == (CRAFT_TICKS) -> 0F
+ else -> 1F
+ }
+
+ val overlayOpacity = if (postCraftTicks < CRAFT_TICKS) menu.getBurnProgressRatio() else 0F
+ renderDisc(matrices, (menu.getBurnProgressRatio() < 1F) && isBurnActive(), baseOpacity, overlayOpacity)
+ }
+ }
+
+ renderScreenSaver(graphics)
+
+ super.render(graphics, mouseX, mouseY, delta)
+
+ renderMoveInfo(graphics, mouseX, mouseY)
+
+ this.renderTooltip(graphics, mouseX, mouseY)
+ }
+
+ override fun renderBlurredBackground(partialTick: Float) {}
+ override fun renderLabels(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int) {}
+
+ fun playSound(soundEvent: SoundEvent) {
+ Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(soundEvent, 1.0F))
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/TMPartySlotWidget.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/TMPartySlotWidget.kt
new file mode 100644
index 0000000000..a56cf62c7a
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/TMPartySlotWidget.kt
@@ -0,0 +1,187 @@
+/*
+ * 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.gui.tmmachine
+
+import com.cobblemon.mod.common.api.gui.blitk
+import com.cobblemon.mod.common.client.gui.drawProfilePokemon
+import com.cobblemon.mod.common.client.render.drawScaledText
+import com.cobblemon.mod.common.client.render.drawScaledTextJustifiedRight
+import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
+import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
+import com.cobblemon.mod.common.pokemon.Gender
+import com.cobblemon.mod.common.pokemon.Pokemon
+import com.cobblemon.mod.common.util.cobblemonResource
+import com.cobblemon.mod.common.util.lang
+import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
+import net.minecraft.client.gui.components.Button
+import net.minecraft.client.sounds.SoundManager
+import net.minecraft.network.chat.Component
+import net.minecraft.network.chat.MutableComponent
+import net.minecraft.resources.ResourceLocation
+import org.joml.Quaternionf
+import org.joml.Vector3f
+
+class TMPartySlotWidget(
+ pX: Number,
+ pY: Number,
+ val pokemon: Pokemon?,
+ onPress: OnPress
+) : Button(pX.toInt(), pY.toInt(), WIDTH, HEIGHT, Component.literal("PartyMember"), onPress, DEFAULT_NARRATION) {
+ val state = FloatingState()
+ var teachable: Int? = null
+ var clickable: Boolean = true
+
+ companion object {
+ const val WIDTH = 57
+ const val HEIGHT = 31
+ private const val PORTRAIT_DIAMETER = 30
+ const val HALF_SCALE = 0.5F
+
+ const val CAN_LEARN = 0
+ const val CANNOT_LEARN = 1
+ const val LEARNED = 2
+
+ private val slotResource = cobblemonResource("textures/gui/tmmachine/party_slot.png")
+ private val slotLearnableResource = cobblemonResource("textures/gui/tmmachine/party_slot_learnable.png")
+ private val slotLearnableDisabledResource = cobblemonResource("textures/gui/tmmachine/party_slot_learnable_disabled.png")
+ private val slotDisabledResource = cobblemonResource("textures/gui/tmmachine/party_slot_disabled.png")
+ private val slotLearnedResource = cobblemonResource("textures/gui/tmmachine/party_slot_learned.png")
+ val genderIconMale = cobblemonResource("textures/gui/pc/gender_icon_male.png")
+ val genderIconFemale = cobblemonResource("textures/gui/pc/gender_icon_female.png")
+
+ private val canLearnLabel = lang("ui.tm_machine.can_learn")
+ private val cannotLearnLabel = lang("ui.tm_machine.cannot_learn")
+ private val learnedLabel = lang("ui.tm_machine.learned")
+ }
+
+ private fun getResource(): ResourceLocation = when (teachable) {
+ CAN_LEARN -> if (clickable) slotLearnableResource else slotLearnableDisabledResource
+ CANNOT_LEARN -> slotDisabledResource
+ LEARNED -> slotLearnedResource
+ else -> slotResource
+ }
+
+ private fun getLabel(): MutableComponent? = when (teachable) {
+ CAN_LEARN -> canLearnLabel
+ CANNOT_LEARN -> cannotLearnLabel
+ LEARNED -> learnedLabel
+ else -> null
+ }
+
+ override fun renderWidget(context: net.minecraft.client.gui.GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ state.currentAspects = pokemon?.aspects ?: emptySet()
+ val matrices = context.pose()
+
+ if (pokemon != null) {
+ blitk(
+ matrixStack = matrices,
+ texture = getResource(),
+ x = x,
+ y = y,
+ width = width,
+ height = height,
+ vOffset = if (clickable && isMouseOver(mouseX.toDouble(), mouseY.toDouble())) height else 0,
+ textureHeight = height * 2,
+ )
+
+ val ballIcon = cobblemonResource("textures/gui/ball/" + pokemon.caughtBall.name.path + ".png")
+ val ballHeight = 22
+ blitk(
+ matrixStack = matrices,
+ texture = ballIcon,
+ x = (x - 2) / HALF_SCALE,
+ y = (y - 3) / HALF_SCALE,
+ height = ballHeight,
+ width = 18,
+ textureHeight = ballHeight * 2,
+ scale = HALF_SCALE
+ )
+
+ // Render Pokémon
+ matrices.pushPose()
+ matrices.translate(x + (PORTRAIT_DIAMETER / 2.0) - 2, y - 1.0, 0.0)
+ matrices.scale(2.5F, 2.5F, 1F)
+ drawProfilePokemon(
+ species = pokemon.species.resourceIdentifier,
+ matrixStack = matrices,
+ rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
+ state = state,
+ scale = 4.5F,
+ partialTicks = delta
+ )
+ matrices.popPose()
+
+ drawScaledTextJustifiedRight(
+ context = context,
+ text = lang("ui.lv.number", pokemon.level),
+ x = x + 54,
+ y = y + 13,
+ shadow = true,
+ scale = HALF_SCALE
+ )
+
+ if (pokemon.gender != Gender.GENDERLESS) {
+ blitk(
+ matrixStack = matrices,
+ texture = if (pokemon.gender == Gender.MALE) genderIconMale else genderIconFemale,
+ x = (x + 51) / HALF_SCALE,
+ y = (y + 19) / HALF_SCALE,
+ height = 8,
+ width = 6,
+ scale = HALF_SCALE
+ )
+ }
+
+ drawScaledTextJustifiedRight(
+ context = context,
+ text = pokemon.getDisplayName(),
+ x = x + ( if (pokemon.gender != Gender.GENDERLESS) 50 else 54),
+ y = y + 19,
+ shadow = true,
+ scale = HALF_SCALE
+ )
+
+ getLabel()?.let { label ->
+ drawScaledText(
+ context = context,
+ text = label,
+ x = x + (width / 2F),
+ y = y + 26,
+ centered = true,
+ scale = HALF_SCALE
+ )
+ }
+
+ // Held Item
+ val heldItem = pokemon.heldItemNoCopy()
+ if (!heldItem.isEmpty) {
+ renderScaledGuiItemIcon(
+ itemStack = heldItem,
+ x = x + 15.0,
+ y = y + 13.5,
+ scale = HALF_SCALE.toDouble(),
+ matrixStack = matrices
+ )
+ }
+ } else {
+ blitk(
+ matrixStack = matrices,
+ texture = slotDisabledResource,
+ x = x + 55,
+ y = y,
+ width = 5,
+ height = height,
+ textureWidth = width,
+ textureHeight = height * 2
+ )
+ }
+ }
+
+ override fun playDownSound(soundManager: SoundManager) {}
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/TypesScrollingWidget.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/TypesScrollingWidget.kt
new file mode 100644
index 0000000000..4e43249bac
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/tmmachine/TypesScrollingWidget.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.gui.tmmachine
+
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.gui.blitk
+import com.cobblemon.mod.common.api.text.text
+import com.cobblemon.mod.common.api.types.ElementalType
+import com.cobblemon.mod.common.api.types.ElementalTypes
+import com.cobblemon.mod.common.client.gui.ScrollingWidget
+import com.cobblemon.mod.common.client.gui.TypeIcon
+import com.cobblemon.mod.common.client.gui.tmmachine.TypesScrollingWidget.ScrollSlotRow
+import com.cobblemon.mod.common.util.cobblemonResource
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.resources.sounds.SimpleSoundInstance
+import net.minecraft.network.chat.Component
+import net.minecraft.util.FastColor
+import net.minecraft.util.Mth
+
+class TypesScrollingWidget(val pX: Int, val pY: Int, val setType: (ElementalType?) -> (Unit)): ScrollingWidget<ScrollSlotRow>(
+ width = WIDTH,
+ height = HEIGHT,
+ left = pX,
+ top = pY - HEIGHT,
+ slotHeight = SLOT_SIZE + SLOT_SPACING
+) {
+ companion object {
+ const val WIDTH = 112
+ const val HEIGHT = 100
+ const val SLOT_SIZE = 22
+ const val SLOT_SPACING = 5
+ }
+
+ init {
+ (listOf(null) + ElementalTypes.all()).chunked(4).forEachIndexed { index, listChunk ->
+ addEntry(ScrollSlotRow(listChunk, setType))
+ }
+ }
+
+ override fun getScrollbarPosition(): Int = rowLeft + width - 3
+
+ override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ val xLeft = this.scrollbarPosition
+ val xRight = xLeft + 3
+ val barHeight = this.bottom - this.y
+
+ var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
+ yBottom = Mth.clamp(yBottom, 32, barHeight - 8)
+ var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + this.y
+ if (yTop < this.y) {
+ yTop = this.y
+ }
+
+ context.fill(xLeft, this.y, xRight, this.bottom, FastColor.ARGB32.color(255, 75, 75, 75)) // background
+ context.fill(xLeft,yTop, xRight, yTop + yBottom, FastColor.ARGB32.color(255, 141, 141, 141)) // base
+ }
+
+ fun renderEntry(context: GuiGraphics, mouseX: Int, mouseY: Int, index: Int, x: Int, y: Int) {
+ val entry = this.getEntry(index)
+ entry.x = x
+ entry.y = y
+ entry.renderRow( context, y, x, mouseX, mouseY)
+ }
+
+ override fun getEntry(index: Int): ScrollSlotRow = children()[index] as ScrollSlotRow
+
+ override fun renderListItems(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ val rowX = rowLeft
+
+ for (index in 0 until this.itemCount) {
+ val rowY = this.getRowTop(index)
+ val rowBottom = this.getRowBottom(index)
+ if (rowBottom >= this.y && rowY <= this.bottom) {
+ this.renderEntry(context!!, mouseX, mouseY, index, rowX, rowY)
+ }
+ }
+ }
+
+ class ScrollSlotRow(val types: List<ElementalType?>, val setType : (ElementalType?) -> Unit): Slot<ScrollSlotRow>() {
+ companion object {
+ private val allSlotResource = cobblemonResource("textures/gui/tmmachine/type_slot_all.png")
+ private val slotResource = cobblemonResource("textures/gui/tmmachine/type_slot.png")
+ }
+
+ var x: Int = 0
+ var y: Int = 0
+
+ fun renderRow(context: GuiGraphics, y: Int, x: Int, mouseX: Int, mouseY: Int) {
+ types.forEachIndexed { index, type ->
+ val matrices = context.pose()
+
+ val startPosX = x + ((SLOT_SPACING + SLOT_SIZE) * index)
+ val startPosY = y + SLOT_SPACING
+
+ val slotHovered = getHoveredSlotIndex(mouseX, mouseY) == index
+
+ blitk(
+ matrixStack = matrices,
+ texture = if (type != null) slotResource else allSlotResource,
+ x = startPosX,
+ y = startPosY,
+ width = SLOT_SIZE,
+ height = SLOT_SIZE,
+ vOffset = if (slotHovered) SLOT_SIZE else 0,
+ textureHeight = SLOT_SIZE * 2
+ )
+
+ if (type != null) {
+ TypeIcon(startPosX + 2, startPosY + 2, type).render(context)
+ }
+ }
+ }
+
+ override fun render(context: GuiGraphics, index: Int, y: Int, x: Int, entryWidth: Int, entryHeight: Int, mouseX: Int, mouseY: Int, hovered: Boolean, tickDelta: Float) {}
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ val hoverIndex = getHoveredSlotIndex(mouseX.toInt(), mouseY.toInt())
+ if (hoverIndex > -1 && hoverIndex < types.size) {
+ setType.invoke(types[hoverIndex])
+ Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
+ }
+ return true
+ }
+
+ private fun getHoveredSlotIndex(mouseX: Int, mouseY: Int): Int {
+ types.forEachIndexed { index, _ ->
+ val startPosX = x + ((SLOT_SPACING + SLOT_SIZE) * index)
+ val startPosY = y + SLOT_SPACING + 1
+
+ if (mouseX in startPosX..(startPosX + SLOT_SIZE) && mouseY in startPosY..(startPosY + SLOT_SIZE)) {
+ return index
+ }
+ }
+ return -1
+ }
+
+ override fun getNarration(): Component {
+ if (types.isNotEmpty()) {
+ return "${types[0]}-${types[types.size - 1]}".text()
+ }
+ return "".text()
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/toast/CobblemonToast.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/toast/CobblemonToast.kt
index f197d20c94..b6f8723abb 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/toast/CobblemonToast.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/toast/CobblemonToast.kt
@@ -26,16 +26,27 @@ import net.minecraft.util.FormattedCharSequence
@Environment(EnvType.CLIENT)
class CobblemonToast(
val id: UUID,
- var icon: ItemStack,
+ var icons: List<ItemStack>,
var title: Component,
var description: Component,
var frameTexture: ResourceLocation,
var progress: Float,
- var progressColor: Int
+ var progressColor: Int,
+ var durationMs: Long? = null
) : Toast {
- constructor(packet: ToastPacket) : this(packet.uuid, packet.icon, packet.title, packet.description, packet.frameTexture, packet.progress, packet.progressColor)
+ constructor(packet: ToastPacket) : this(
+ packet.uuid,
+ packet.icons,
+ packet.title,
+ packet.description,
+ packet.frameTexture,
+ packet.progress,
+ packet.progressColor,
+ packet.durationMs
+ )
+ private var startTime: Long = -1
private var lastProgress = 0F
private var lastTime = 0L
internal var nextVisibility: Toast.Visibility = Toast.Visibility.SHOW
@@ -45,6 +56,17 @@ class CobblemonToast(
}
override fun render(context: GuiGraphics, manager: ToastComponent, startTime: Long): Toast.Visibility {
+ if (this.startTime == -1L) this.startTime = startTime
+
+ val elapsedTime = startTime - this.startTime
+ val localDuration = this.durationMs
+ if (localDuration != null && elapsedTime >= localDuration) {
+ ToastTracker.remove(this)
+ return Toast.Visibility.HIDE
+ }
+
+ context.blitSprite(this.frameTexture, 0, 0, this.width(), this.height())
+
val maxWidth = 125
val textRenderer = manager.minecraft.font
@@ -52,19 +74,38 @@ class CobblemonToast(
context.drawString(textRenderer, this.title, 30, 7, this.title.style.color?.value ?: -1, false)
context.drawWordWrap(textRenderer, this.description, 30, 18, maxWidth, this.description.style.color?.value ?: -1)
- context.renderFakeItem(this.icon, 8, 8)
+
+ val iconIndex = if (localDuration != null && localDuration > 0) {
+ val repeatsPerIcon = 2
+ val totalIcons = icons.size * repeatsPerIcon
+ val timePerIcon = (localDuration / totalIcons).coerceAtLeast(1L)
+
+ val iconIndex = (elapsedTime / timePerIcon)
+ .toInt()
+ .coerceIn(0, totalIcons - 1)
+
+ iconIndex / repeatsPerIcon
+ } else {
+ val switchIntervalMs = 500L
+ ((elapsedTime / switchIntervalMs) % icons.size).toInt()
+ }
+
+ val icon = this.icons[iconIndex]
+ context.renderFakeItem(icon, 8, 8)
if (this.hasProgressBar()) {
context.fill(3, 28, 157, 29, -1)
val f = Mth.clampedLerp(this.lastProgress, this.progress, (startTime - this.lastTime).toFloat() / 100F)
context.fill(3, 28, (3F + 154F * f).toInt(), 29, this.progressColor)
this.lastProgress = f
}
+
this.lastTime = startTime
return this.nextVisibility
}
internal fun updateFrom(packet: ToastPacket) {
- this.icon = packet.icon
+ val newIcons = this.icons + packet.icons
+ this.icons = newIcons
this.title = packet.title
this.description = packet.description
this.frameTexture = packet.frameTexture
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/toast/ToastTracker.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/toast/ToastTracker.kt
index 4c178b4c8d..0faf912f86 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/toast/ToastTracker.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/gui/toast/ToastTracker.kt
@@ -30,4 +30,7 @@ object ToastTracker {
}
}
+ fun remove(toast: CobblemonToast) {
+ toasts.remove(toast.id)
+ }
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/net/gui/OpenTMMPacketHandler.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/net/gui/OpenTMMPacketHandler.kt
new file mode 100644
index 0000000000..a7c51a68d4
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/net/gui/OpenTMMPacketHandler.kt
@@ -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.client.net.gui
+
+import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
+import com.cobblemon.mod.common.net.messages.client.ui.OpenTMMPacket
+import net.minecraft.client.Minecraft
+
+object OpenTMMPacketHandler : ClientNetworkPacketHandler<OpenTMMPacket> {
+ override fun handle(packet: OpenTMMPacket, client: Minecraft) {
+ //client.setScreen()
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/net/gui/SetActiveTMPacketHandler.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/net/gui/SetActiveTMPacketHandler.kt
new file mode 100644
index 0000000000..7f44796473
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/net/gui/SetActiveTMPacketHandler.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.gui
+
+import com.cobblemon.mod.common.Cobblemon
+import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
+import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
+import com.cobblemon.mod.common.net.messages.client.ui.SetActiveTMPacket
+import net.minecraft.server.MinecraftServer
+import net.minecraft.server.level.ServerPlayer
+import net.minecraft.world.level.block.Block
+
+object SetActiveTMPacketHandler : ServerNetworkPacketHandler<SetActiveTMPacket> {
+ override fun handle(packet: SetActiveTMPacket, server: MinecraftServer, player: ServerPlayer) {
+ val menu = player.containerMenu as? TMMachineMenu ?: return
+ val inventory = menu.inventory ?: return
+ val tmData = Cobblemon.playerDataManager.getTMData(player)
+
+ if (packet.tm != null && (!packet.tm.isPassivelyObtained() && packet.tm.id !in tmData.learnedTMs)) {
+ return // Hacker smh
+ }
+
+ menu.tmMachineEntity?.let { blockEntity ->
+ blockEntity.activeMove = packet.tm?.moveName?.name ?: ""
+ blockEntity.setChanged()
+ player.level().sendBlockUpdated(blockEntity.blockPos, blockEntity.blockState, blockEntity.blockState, Block.UPDATE_CLIENTS)
+ }
+
+ inventory.setChanged()
+ menu.broadcastChanges()
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/net/gui/SetTMMachineContainerDataPacketHandler.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/net/gui/SetTMMachineContainerDataPacketHandler.kt
new file mode 100644
index 0000000000..3e4d9ab5f0
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/net/gui/SetTMMachineContainerDataPacketHandler.kt
@@ -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.client.net.gui
+
+import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
+import com.cobblemon.mod.common.block.entity.TMMachineBlockEntity
+import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
+import com.cobblemon.mod.common.net.messages.client.ui.SetTMMachineContainerDataPacket
+import net.minecraft.server.MinecraftServer
+import net.minecraft.server.level.ServerPlayer
+
+object SetTMMachineContainerDataPacketHandler : ServerNetworkPacketHandler<SetTMMachineContainerDataPacket> {
+
+ override fun handle(packet: SetTMMachineContainerDataPacket, server: MinecraftServer, player: ServerPlayer) {
+ val menu = player.containerMenu as? TMMachineMenu ?: return
+ val slotIndex = packet.dataSlotIndex
+ val slotValue = packet.value
+
+ menu.containerData?.set(slotIndex, slotValue)
+
+ // If setting to not active, also reset progress
+ if (slotIndex == TMMachineBlockEntity.BURN_ACTIVE_INDEX && slotValue == 0) {
+ menu.containerData?.set(TMMachineBlockEntity.BURN_PROGRESS_INDEX, 0)
+ }
+
+ menu.broadcastChanges()
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/net/settings/ServerSettingsPacketHandler.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/net/settings/ServerSettingsPacketHandler.kt
index 5378c110a4..cb4e49e86d 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/net/settings/ServerSettingsPacketHandler.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/net/settings/ServerSettingsPacketHandler.kt
@@ -21,6 +21,7 @@ object ServerSettingsPacketHandler : ClientNetworkPacketHandler<ServerSettingsPa
ServerSettings.maxPokemonLevel = packet.maxPokemonLevel
ServerSettings.maxPokemonFriendship = packet.maxPokemonFriendship
ServerSettings.maxDynamaxLevel = packet.maxDynamaxLevel
+ ServerSettings.unlockAllMoveDexMovesByDefault = packet.unlockAllMoveDexMovesByDefault
}
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/render/block/TMShelfBlockEntityRenderer.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/render/block/TMShelfBlockEntityRenderer.kt
new file mode 100644
index 0000000000..2e0ebd1440
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/render/block/TMShelfBlockEntityRenderer.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.block.entity.TMShelfBlockEntity
+import com.cobblemon.mod.common.CobblemonItems
+import com.cobblemon.mod.common.client.render.color.TechnicalMachineItemColorProvider
+import com.cobblemon.mod.common.util.cobblemonResource
+import com.mojang.blaze3d.vertex.PoseStack
+import com.mojang.blaze3d.vertex.VertexConsumer
+import com.mojang.math.Axis
+import net.minecraft.client.renderer.LevelRenderer
+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.core.Direction
+import net.minecraft.core.registries.BuiltInRegistries
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.util.FastColor
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.level.BlockAndTintGetter
+import net.minecraft.world.level.block.HorizontalDirectionalBlock
+import org.joml.Matrix4f
+
+class TMShelfBlockEntityRenderer(ctx: BlockEntityRendererProvider.Context) :
+ BlockEntityRenderer<TMShelfBlockEntity> {
+
+ private val slotWidthPixels = 6
+ private val slotHeightPixels = 2
+ private val textureSize = 16f
+
+ private val leftMargin = 1f
+ private val rightMargin = 1f
+ private val topMargin = 1f
+ private val bottomMargin = 1f
+ private val interRowSpacing = 0f
+ private val tmOverlayDepthOffset = -0.0002f
+ private val slotBrightnessMultiplier = 0.65f
+
+ private val tmBaseTexture = cobblemonResource("textures/block/tm_shelf/technical_machine_base.png")
+ private val tmOverlayTexture = cobblemonResource("textures/block/tm_shelf/technical_machine_overlay.png")
+
+ override fun render(
+ entity: TMShelfBlockEntity,
+ partialTicks: Float,
+ poseStack: PoseStack,
+ buffer: MultiBufferSource,
+ light: Int,
+ overlay: Int
+ ) {
+ val facing = entity.blockState.getValue(HorizontalDirectionalBlock.FACING)
+ val rotation = when (facing) {
+ Direction.NORTH -> 0f
+ Direction.SOUTH -> 180f
+ Direction.WEST -> 90f
+ Direction.EAST -> -90f
+ else -> 0f
+ }
+
+ val lightGetter = entity.level as? BlockAndTintGetter
+ val packedLight = lightGetter?.let {
+ LevelRenderer.getLightColor(it, entity.blockPos.relative(facing))
+ } ?: if (light != 0) light else 0xF000F0
+
+ poseStack.pushPose()
+ poseStack.translate(0.5, 0.5, 0.5)
+ poseStack.mulPose(Axis.YP.rotationDegrees(rotation))
+ poseStack.translate(-0.5, -0.5, -0.5)
+
+ val slotWidth = slotWidthPixels / textureSize
+ val slotHeight = slotHeightPixels / textureSize
+ val verticalSpacing = (slotHeightPixels + interRowSpacing) / textureSize
+ val totalHeight = slotHeight * 7 + interRowSpacing * 6 / textureSize
+ val startY = topMargin / textureSize
+
+ for ((index, item) in entity.items.withIndex()) {
+ if (!item.isEmpty) {
+ val row = index / 2
+ val col = index % 2
+ val visualCol = if (facing == Direction.NORTH || facing == Direction.SOUTH) 1 - col else col
+
+ val isLeftColumn = visualCol == 0
+ val posX = if (isLeftColumn)
+ leftMargin / textureSize
+ else
+ (16f - rightMargin - slotWidthPixels) / textureSize
+
+ val posY = startY + (6 - row) * verticalSpacing
+
+ poseStack.pushPose()
+ poseStack.translate(posX, posY, -0.001f)
+
+ if (item.item == CobblemonItems.TECHNICAL_MACHINE) {
+ val baseColor = TechnicalMachineItemColorProvider.getColor(item, 0)
+ val overlayColor = TechnicalMachineItemColorProvider.getColor(item, 1)
+
+ renderSlotQuad(
+ poseStack,
+ buffer,
+ tmBaseTexture,
+ baseColor,
+ packedLight,
+ slotWidth,
+ slotHeight,
+ tint = true,
+ useTextRenderType = true
+ )
+
+ poseStack.pushPose()
+ poseStack.translate(0.0, 0.0, tmOverlayDepthOffset.toDouble())
+ renderSlotQuad(
+ poseStack,
+ buffer,
+ tmOverlayTexture,
+ overlayColor,
+ packedLight,
+ slotWidth,
+ slotHeight,
+ tint = true,
+ useTextRenderType = true
+ )
+ poseStack.popPose()
+ } else {
+ val tex = getItemTexture(item)
+ renderSlotQuad(
+ poseStack,
+ buffer,
+ tex,
+ 0xFFFFFF,
+ packedLight,
+ slotWidth,
+ slotHeight,
+ tint = false,
+ useTextRenderType = true
+ )
+ }
+
+ poseStack.popPose()
+ }
+ }
+
+ poseStack.popPose()
+ }
+
+ private fun renderSlotQuad(
+ poseStack: PoseStack,
+ buffer: MultiBufferSource,
+ texture: ResourceLocation,
+ color: Int,
+ light: Int,
+ width: Float,
+ height: Float,
+ tint: Boolean,
+ useTextRenderType: Boolean
+ ) {
+ val renderType = if (useTextRenderType) RenderType.text(texture) else RenderType.entityCutout(texture)
+ val consumer: VertexConsumer = buffer.getBuffer(renderType)
+ val matrix: Matrix4f = poseStack.last().pose()
+
+ val (rBase, gBase, bBase, a) = if (tint) {
+ listOf(
+ FastColor.ARGB32.red(color) / 255f,
+ FastColor.ARGB32.green(color) / 255f,
+ FastColor.ARGB32.blue(color) / 255f,
+ 1.0f
+ )
+ } else listOf(1f, 1f, 1f, 1f)
+ val r = (rBase * slotBrightnessMultiplier).coerceIn(0f, 1f)
+ val g = (gBase * slotBrightnessMultiplier).coerceIn(0f, 1f)
+ val b = (bBase * slotBrightnessMultiplier).coerceIn(0f, 1f)
+
+ consumer.addVertex(matrix, 0f, 0f, 0f).setColor(r, g, b, a).setUv(0f, 1f)
+ .setUv1(0, 0).setUv2(light and 0xFFFF, light shr 16).setNormal(0f, 0f, -1f)
+ consumer.addVertex(matrix, 0f, height, 0f).setColor(r, g, b, a).setUv(0f, 0f)
+ .setUv1(0, 0).setUv2(light and 0xFFFF, light shr 16).setNormal(0f, 0f, -1f)
+ consumer.addVertex(matrix, width, height, 0f).setColor(r, g, b, a).setUv(1f, 0f)
+ .setUv1(0, 0).setUv2(light and 0xFFFF, light shr 16).setNormal(0f, 0f, -1f)
+ consumer.addVertex(matrix, width, 0f, 0f).setColor(r, g, b, a).setUv(1f, 1f)
+ .setUv1(0, 0).setUv2(light and 0xFFFF, light shr 16).setNormal(0f, 0f, -1f)
+ }
+
+ private fun getItemTexture(itemStack: ItemStack): ResourceLocation {
+ val id = BuiltInRegistries.ITEM.getKey(itemStack.item)
+ return cobblemonResource("textures/block/tm_shelf/${id.path}.png")
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/render/color/TechnicalMachineItemColorProvider.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/render/color/TechnicalMachineItemColorProvider.kt
new file mode 100644
index 0000000000..0290cb329c
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/render/color/TechnicalMachineItemColorProvider.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.color
+
+import com.cobblemon.mod.common.api.types.ElementalTypes
+import com.cobblemon.mod.common.item.components.TMMoveComponent.Companion.getTMMove
+import net.minecraft.client.color.item.ItemColor
+import net.minecraft.util.FastColor
+import net.minecraft.world.item.ItemStack
+
+object TechnicalMachineItemColorProvider : ItemColor {
+ override fun getColor(itemStack: ItemStack, layer: Int): Int {
+ val moveType = getTMMove(itemStack)?.elementalType ?: ElementalTypes.NORMAL
+ val color = if (layer == 0) moveType.primaryColor else moveType.secondaryColor
+ return FastColor.ARGB32.opaque(color)
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/settings/ServerSettings.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/settings/ServerSettings.kt
index 27606dbeb4..b241b3183e 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/client/settings/ServerSettings.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/settings/ServerSettings.kt
@@ -27,5 +27,6 @@ object ServerSettings {
var maxPokemonLevel = Cobblemon.config.maxPokemonLevel
var maxPokemonFriendship = Cobblemon.config.maxPokemonFriendship
var maxDynamaxLevel = Cobblemon.config.maxDynamaxLevel
+ var unlockAllMoveDexMovesByDefault = Cobblemon.config.unlockAllMoveDexMovesByDefault
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/client/tooltips/TechnicalMachineTooltipGenerator.kt b/common/src/main/kotlin/com/cobblemon/mod/common/client/tooltips/TechnicalMachineTooltipGenerator.kt
new file mode 100644
index 0000000000..a9def727d6
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/client/tooltips/TechnicalMachineTooltipGenerator.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.tooltips
+
+import com.cobblemon.mod.common.api.text.*
+import com.cobblemon.mod.common.item.interactive.TechnicalMachineItem
+import com.cobblemon.mod.common.item.components.TMMoveComponent
+import com.cobblemon.mod.common.util.lang
+import net.minecraft.core.component.DataComponents
+import net.minecraft.network.chat.Component
+import net.minecraft.world.item.ItemStack
+
+object TechnicalMachineTooltipGenerator : TooltipGenerator() {
+ private val technicalMachineHeader by lazy { lang("technical_machine_effect_header").blue() }
+ private val technicalMachineItemClass by lazy { lang("item_class.technical_machine").blue() }
+
+ // Max characters per description line
+ private const val MAX_LINE_LENGTH = 35
+
+ override fun generateCategoryTooltip(stack: ItemStack, lines: MutableList<Component>): MutableList<Component>? {
+ if (stack.get(DataComponents.HIDE_ADDITIONAL_TOOLTIP) != null) return null
+ if (stack.item !is TechnicalMachineItem) return null
+ return mutableListOf(technicalMachineItemClass)
+ }
+
+ override fun generateAdditionalTooltip(stack: ItemStack, lines: MutableList<Component>): MutableList<Component>? {
+ if (stack.get(DataComponents.HIDE_ADDITIONAL_TOOLTIP) != null) return null
+ if (stack.item !is TechnicalMachineItem) return null
+
+ val move = TMMoveComponent.getTMMove(stack) ?: return null
+
+ val result = mutableListOf<Component>()
+
+ result.add(technicalMachineHeader)
+
+ // Type
+ val typeLabel = lang("ui.info.type").white()
+ val typeValue = move.elementalType.displayName.copy().withStyle { it.withColor(move.elementalType.primaryColor) }
+
+ // Category
+ val categoryLabel = lang("move.category").white()
+ val categoryKey = "move.category.${move.damageCategory.name.lowercase()}"
+ val categoryValue = lang(categoryKey).gold()
+
+ result.add(
+ Component.empty()
+ .append(typeLabel).append(": ").append(typeValue)
+ .append(Component.literal(" "))
+ .append(categoryLabel).append(": ").append(categoryValue)
+ )
+
+ // Power / Accuracy / PP
+ val powerLabel = lang("ui.power").white()
+ val accuracyLabel = lang("ui.accuracy").white()
+ val ppLabel = lang("ui.pp").white()
+
+ val accuracy = move.accuracy.toInt()
+
+ val powerValue = Component.literal(move.power.toInt().toString()).yellow()
+ val accuracyValue = Component.literal("${if (accuracy != -1) accuracy else 100}%").yellow()
+ val ppValue = Component.literal(move.pp.toString()).yellow()
+
+ result.add(
+ Component.empty()
+ .append(powerLabel).append(": ").append(powerValue)
+ .append(Component.literal(" "))
+ .append(accuracyLabel).append(": ").append(accuracyValue)
+ .append(Component.literal(" "))
+ .append(ppLabel).append(": ").append(ppValue)
+ )
+
+ // Description (wrapped)
+ val description = move.description.string
+ val wrappedLines = wrapText(description, MAX_LINE_LENGTH)
+ for (line in wrappedLines) {
+ result.add(Component.literal(line).gray())
+ }
+
+ return result
+ }
+
+ private fun wrapText(text: String, maxLength: Int): List<String> {
+ val words = text.split(" ")
+ val lines = mutableListOf<String>()
+ var currentLine = StringBuilder()
+
+ for (word in words) {
+ if (currentLine.length + word.length + 1 > maxLength) {
+ lines.add(currentLine.toString())
+ currentLine = StringBuilder()
+ }
+ if (currentLine.isNotEmpty()) currentLine.append(" ")
+ currentLine.append(word)
+ }
+
+ if (currentLine.isNotEmpty()) {
+ lines.add(currentLine.toString())
+ }
+
+ return lines
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/command/GiveTmCommand.kt b/common/src/main/kotlin/com/cobblemon/mod/common/command/GiveTmCommand.kt
new file mode 100644
index 0000000000..71cee4207d
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/command/GiveTmCommand.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.command
+
+import com.cobblemon.mod.common.api.permission.CobblemonPermissions
+import com.cobblemon.mod.common.command.argument.MoveArgumentType
+import com.cobblemon.mod.common.item.components.TMMoveComponent
+import com.cobblemon.mod.common.util.alias
+import com.cobblemon.mod.common.util.permission
+import com.mojang.brigadier.Command
+import com.mojang.brigadier.CommandDispatcher
+import com.mojang.brigadier.context.CommandContext
+import com.mojang.brigadier.arguments.IntegerArgumentType
+import net.minecraft.commands.CommandSourceStack
+import net.minecraft.commands.Commands
+import net.minecraft.commands.arguments.EntityArgument
+import net.minecraft.network.chat.Component
+import net.minecraft.server.level.ServerPlayer
+
+object GiveTmCommand {
+
+ private const val NAME = "givetm"
+ private const val ALIAS = "giveTM"
+ private const val PLAYER = "player"
+ private const val MOVE = "move"
+ private const val QUANTITY = "quantity"
+
+ fun register(dispatcher: CommandDispatcher<CommandSourceStack>) {
+ val command = Commands.literal(NAME)
+ .permission(CobblemonPermissions.TECHNICAL_MACHINE)
+ .then(
+ Commands.argument(MOVE, MoveArgumentType.move())
+ .executes { ctx ->
+ execute(ctx, ctx.source.playerOrException, 1)
+ }
+ .then(
+ Commands.argument(QUANTITY, IntegerArgumentType.integer(1, 2304))
+ .executes { ctx ->
+ execute(ctx, ctx.source.playerOrException, IntegerArgumentType.getInteger(ctx, QUANTITY))
+ }
+ )
+ .then(
+ Commands.argument(PLAYER, EntityArgument.player())
+ .executes { ctx ->
+ execute(ctx, EntityArgument.getPlayer(ctx, PLAYER), 1)
+ }
+ .then(
+ Commands.argument(QUANTITY, IntegerArgumentType.integer(1, 2304))
+ .executes { ctx ->
+ execute(
+ ctx,
+ EntityArgument.getPlayer(ctx, PLAYER),
+ IntegerArgumentType.getInteger(ctx, QUANTITY)
+ )
+ }
+ )
+ )
+ )
+
+ val node = dispatcher.register(command)
+ dispatcher.register(node.alias(ALIAS))
+ }
+
+ private fun execute(context: CommandContext<CommandSourceStack>, player: ServerPlayer, quantity: Int): Int {
+ val move = MoveArgumentType.getMove(context, MOVE)
+
+ var remaining = quantity
+ while (remaining > 0) {
+ val stackSize = remaining.coerceAtMost(64)
+ val stack = TMMoveComponent.createStack(move).copyWithCount(stackSize)
+ if (!player.addItem(stack)) {
+ player.drop(stack, false)
+ }
+ remaining -= stackSize
+ }
+
+ context.source.sendSuccess(
+ { Component.literal("Gave $quantity TM ${move.displayName.string} to ${player.name.string}") },
+ true
+ )
+ return Command.SINGLE_SUCCESS
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/command/TmCommand.kt b/common/src/main/kotlin/com/cobblemon/mod/common/command/TmCommand.kt
new file mode 100644
index 0000000000..68fa415f79
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/command/TmCommand.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.command
+
+import com.cobblemon.mod.common.Cobblemon
+import com.cobblemon.mod.common.api.permission.CobblemonPermissions
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.command.argument.TmArgumentType
+import com.cobblemon.mod.common.util.permission
+import com.mojang.brigadier.Command
+import com.mojang.brigadier.CommandDispatcher
+import com.mojang.brigadier.context.CommandContext
+import net.minecraft.commands.CommandSourceStack
+import net.minecraft.commands.Commands
+import net.minecraft.commands.arguments.EntityArgument
+import net.minecraft.network.chat.Component
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.server.level.ServerPlayer
+
+object TmCommand {
+
+ private const val NAME = "technicalmachine"
+ private const val UNLOCK = "unlock"
+ private const val LOCK = "lock"
+ private const val CHECK = "check"
+ private const val PLAYER = "player"
+ private const val ONLY = "only"
+ private const val ALL = "all"
+ private const val TM = "TM"
+
+ fun register(dispatcher: CommandDispatcher<CommandSourceStack>) {
+ val command =
+ Commands.literal(NAME)
+ .permission(CobblemonPermissions.TECHNICAL_MACHINE)
+ .then(Commands.literal(UNLOCK)
+ .then(Commands.argument(PLAYER, EntityArgument.players())
+ .then(Commands.literal(ONLY)
+ .then(Commands.argument(TM, TmArgumentType.tm())
+ .executes { ctx ->
+ executeUnlockOnly(ctx, EntityArgument.getPlayers(ctx, PLAYER))
+ }
+ )
+ )
+ .then(Commands.literal(ALL)
+ .executes { ctx ->
+ executeUnlockAll(ctx, EntityArgument.getPlayers(ctx, PLAYER))
+ }
+ )
+ )
+ )
+ .then(Commands.literal(LOCK)
+ .then(Commands.argument(PLAYER, EntityArgument.players())
+ .then(Commands.literal(ONLY)
+ .then(Commands.argument(TM, TmArgumentType.tm())
+ .executes { ctx ->
+ executeLockOnly(ctx, EntityArgument.getPlayers(ctx, PLAYER))
+ }
+ )
+ )
+ .then(Commands.literal(ALL)
+ .executes { ctx ->
+ executeLockAll(ctx, EntityArgument.getPlayers(ctx, PLAYER))
+ }
+ )
+ )
+ )
+ .then(Commands.literal(CHECK)
+ .then(Commands.argument(PLAYER, EntityArgument.players())
+ .executes { ctx ->
+ executeCheck(ctx, EntityArgument.getPlayer(ctx, PLAYER))
+ }
+ )
+ )
+
+ dispatcher.register(command)
+ }
+
+ private fun executeUnlockOnly(context: CommandContext<CommandSourceStack>, players: Collection<ServerPlayer>) : Int {
+ val tm = TmArgumentType.getTm(context, TM)
+
+ players.forEach { player ->
+ val playerTMData = Cobblemon.playerDataManager.getTMData(player.uuid)
+ playerTMData.learn(listOf(tm.id))
+ }
+
+ val selectorStr =
+ if (players.size == 1) players.first().name.string
+ else "${players.size} players"
+
+ context.source.sendSystemMessage(
+ Component.literal("Unlocked TM ${tm.id} for $selectorStr")
+ )
+
+ return Command.SINGLE_SUCCESS
+ }
+
+ private fun executeLockOnly(context: CommandContext<CommandSourceStack>, players: Collection<ServerPlayer>) : Int {
+ val tm = TmArgumentType.getTm(context, TM)
+
+ players.forEach { player ->
+ val playerTMData = Cobblemon.playerDataManager.getTMData(player.uuid)
+ playerTMData.unlearn(listOf(tm.id))
+ }
+
+ val selectorStr =
+ if (players.size == 1) players.first().name.string
+ else "${players.size} players"
+
+ context.source.sendSystemMessage(
+ Component.literal("Locked TM ${tm.id} for $selectorStr")
+ )
+
+ return Command.SINGLE_SUCCESS
+ }
+
+ private fun executeUnlockAll(context: CommandContext<CommandSourceStack>, players: Collection<ServerPlayer>) : Int {
+ val allTms = TechnicalMachines.getAllResourceLocations()
+
+ players.forEach { player ->
+ val playerTMData = Cobblemon.playerDataManager.getTMData(player.uuid)
+ playerTMData.learn(allTms)
+ }
+
+ val selectorStr =
+ if (players.size == 1) players.first().name.string
+ else "${players.size} players"
+
+ context.source.sendSystemMessage(
+ Component.literal("Unlocked ${allTms.size} TMs for $selectorStr")
+ )
+
+ return Command.SINGLE_SUCCESS
+ }
+
+ private fun executeLockAll(context: CommandContext<CommandSourceStack>, players: Collection<ServerPlayer>) : Int {
+ val allTms = TechnicalMachines.getAllResourceLocations()
+
+ players.forEach { player ->
+ val playerTMData = Cobblemon.playerDataManager.getTMData(player.uuid)
+ playerTMData.unlearn(allTms)
+ }
+
+ val selectorStr =
+ if (players.size == 1) players.first().name.string
+ else "${players.size} players"
+
+ context.source.sendSystemMessage(
+ Component.literal("Locked ${allTms.size} TMs for $selectorStr")
+ )
+
+ return Command.SINGLE_SUCCESS
+ }
+
+ private fun executeCheck(context: CommandContext<CommandSourceStack>, player: ServerPlayer) : Int {
+ val playerTMData = Cobblemon.playerDataManager.getTMData(player.uuid)
+ val playerPc = Cobblemon.storage.getPC(player)
+ val playerParty = Cobblemon.storage.getParty(player)
+ val allPlayerPokemon = playerPc.toList() + playerParty.toList()
+
+ val movesToLearn = mutableSetOf<ResourceLocation>()
+ for (pokemon in allPlayerPokemon) {
+ val learnableMoves = playerTMData.getLearnableTMsFromPokemon(pokemon)
+ movesToLearn.addAll(learnableMoves)
+ }
+
+ playerTMData.learn(movesToLearn)
+
+ context.source.sendSystemMessage(
+ Component.literal("Unlocked ${movesToLearn.size} TMs for ${player.name.string}")
+ )
+
+ return Command.SINGLE_SUCCESS
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/command/argument/TmArgumentType.kt b/common/src/main/kotlin/com/cobblemon/mod/common/command/argument/TmArgumentType.kt
new file mode 100644
index 0000000000..e01b231d38
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/command/argument/TmArgumentType.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.command.argument
+
+import com.cobblemon.mod.common.api.tms.TechnicalMachine
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.util.commandLang
+import com.mojang.brigadier.StringReader
+import com.mojang.brigadier.arguments.ArgumentType
+import com.mojang.brigadier.context.CommandContext
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType
+import com.mojang.brigadier.suggestion.Suggestions
+import com.mojang.brigadier.suggestion.SuggestionsBuilder
+import net.minecraft.commands.SharedSuggestionProvider
+import net.minecraft.network.chat.MutableComponent
+import net.minecraft.resources.ResourceLocation
+import java.util.concurrent.CompletableFuture
+
+class TmArgumentType : ArgumentType<TechnicalMachine> {
+
+ override fun parse(reader: StringReader): TechnicalMachine {
+ val resourceLocation = ResourceLocation.read(reader)
+ if (resourceLocation == null) {
+ throw SimpleCommandExceptionType(INVALID_TM).createWithContext(reader)
+ }
+
+ val tm = TechnicalMachines.getByResourceLocation(resourceLocation)
+ if (tm == null) {
+ throw SimpleCommandExceptionType(INVALID_TM).createWithContext(reader)
+ }
+
+ return tm
+ }
+
+ override fun <S : Any> listSuggestions(
+ context: CommandContext<S>,
+ builder: SuggestionsBuilder
+ ): CompletableFuture<Suggestions> {
+ val resourceLocations = TechnicalMachines.getAllResourceLocations()
+ val suggestions = resourceLocations.map { it.toString() }
+ return SharedSuggestionProvider.suggest(suggestions, builder)
+ }
+
+ companion object {
+ val INVALID_TM: MutableComponent = commandLang("technicalmachine.invalid-tm")
+
+ fun tm() = TmArgumentType()
+
+ fun <S> getTm(context: CommandContext<S>, name: String): TechnicalMachine {
+ return context.getArgument(name, TechnicalMachine::class.java)
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/config/CobblemonConfig.kt b/common/src/main/kotlin/com/cobblemon/mod/common/config/CobblemonConfig.kt
index d5b1476244..cb9cafe597 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/config/CobblemonConfig.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/config/CobblemonConfig.kt
@@ -274,6 +274,12 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Pokedex, lang = "max_pokedex_scanning_detection_range", SERVER)
var maxPokedexScanningDetectionRange = 10.0
+ @CobblemonConfigField(Category.Pokedex, lang = "unlock_all_move_dex_moves_by_default", SERVER)
+ var unlockAllMoveDexMovesByDefault = false
+
+ @CobblemonConfigField(Category.Pokemon, lang = "infinite_tm_uses", SERVER)
+ var infiniteTmUses = false
+
@CobblemonConfigField(Category.Pokedex, lang = "hide_unimplemented_pokemon_in_the_pokedex", CLIENT)
var hideUnimplementedPokemonInThePokedex = false
@@ -360,4 +366,4 @@ class CobblemonConfig {
}
return newConfig
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/data/CobblemonDataProvider.kt b/common/src/main/kotlin/com/cobblemon/mod/common/data/CobblemonDataProvider.kt
index 4b4794d84a..044aa64da4 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/data/CobblemonDataProvider.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/data/CobblemonDataProvider.kt
@@ -48,6 +48,7 @@ import com.cobblemon.mod.common.api.scripting.CobblemonScripts
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.SpawnDetailPresets
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.pokemon.SpeciesAdditions
@@ -111,6 +112,7 @@ object CobblemonDataProvider : DataProvider {
this.register(PokemonInteractions, reloadable = false)
this.register(SpawnBaitEffects, reloadable = false)
this.register(CobblemonRideSettings, reloadable = true)
+ this.register(TechnicalMachines, reloadable = false)
SpawnBait.Effects.setupEffects()
PlatformEvents.SERVER_PLAYER_LOGOUT.subscribe {
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/events/AdvancementHandler.kt b/common/src/main/kotlin/com/cobblemon/mod/common/events/AdvancementHandler.kt
index 275e159533..f4374f4aae 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/events/AdvancementHandler.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/events/AdvancementHandler.kt
@@ -31,6 +31,7 @@ import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.util.effectiveName
import com.cobblemon.mod.common.util.getPlayer
+import com.cobblemon.mod.common.util.tmList
import java.util.*
object AdvancementHandler : EventHandler {
@@ -85,6 +86,7 @@ object AdvancementHandler : EventHandler {
fun onEvolve(event: EvolutionCompleteEvent) {
val player = event.pokemon.getOwnerPlayer()
if (player != null) {
+ player.tmList()?.syncTMsFromPokemon(event.pokemon)
if (event.pokemon.preEvolution != null) {
val playerData = Cobblemon.playerDataManager.getGenericData(player)
val advancementData = playerData.advancementData
@@ -145,6 +147,7 @@ object AdvancementHandler : EventHandler {
}
fun onLevelUp(event : LevelUpEvent) {
+ event.pokemon.getOwnerPlayer()?.tmList()?.syncTMsFromPokemon(event.pokemon)
event.pokemon.getOwnerPlayer()?.let { CobblemonCriteria.LEVEL_UP.trigger(it, LevelUpContext(event.newLevel, event.pokemon)) }
}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/events/PokedexHandler.kt b/common/src/main/kotlin/com/cobblemon/mod/common/events/PokedexHandler.kt
index 36e56fc763..8028f20b59 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/events/PokedexHandler.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/events/PokedexHandler.kt
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.events
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.events.CobblemonEvents
+import com.cobblemon.mod.common.api.events.pokemon.LevelUpEvent
import com.cobblemon.mod.common.api.events.pokemon.PokemonAspectsChangedEvent
import com.cobblemon.mod.common.api.events.pokemon.PokemonGainedEvent
import com.cobblemon.mod.common.api.events.pokemon.PokemonSeenEvent
@@ -20,6 +21,7 @@ object PokedexHandler : EventHandler {
CobblemonEvents.POKEMON_GAINED.subscribe(Priority.NORMAL, ::onPokemonGained)
CobblemonEvents.POKEMON_SEEN.subscribe(Priority.NORMAL, ::onPokemonSeen)
CobblemonEvents.POKEMON_ASPECTS_CHANGED.subscribe(Priority.NORMAL, ::onPokemonAspectsChanged)
+ CobblemonEvents.LEVEL_UP_EVENT.subscribe(Priority.NORMAL, ::onLevelUp)
}
fun onPokemonGained(event: PokemonGainedEvent) {
@@ -35,4 +37,8 @@ object PokedexHandler : EventHandler {
Cobblemon.playerDataManager.getPokedexData(event.ownerId).catch(event.pokemon)
}
}
+
+ fun onLevelUp(event: LevelUpEvent) {
+ Cobblemon.playerDataManager.getPokedexData(event.pokemon.getOwnerUUID() ?: return).obtain(event.pokemon)
+ }
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/item/TypeGemItem.kt b/common/src/main/kotlin/com/cobblemon/mod/common/item/TypeGemItem.kt
new file mode 100644
index 0000000000..7bb73ad822
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/item/TypeGemItem.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.item
+
+import com.cobblemon.mod.common.block.TypeGemBlock
+import com.cobblemon.mod.common.block.TypeGemClusterBlock
+import com.cobblemon.mod.common.block.TypeGemCoreBlock
+import net.minecraft.world.InteractionResult
+import net.minecraft.world.item.Item
+import net.minecraft.world.item.context.UseOnContext
+import net.minecraft.world.level.block.Block
+import net.minecraft.world.level.block.DirectionalBlock
+
+class TypeGemItem(
+ private val clusterBlock: Block,
+ properties: Properties
+) : Item(properties) {
+
+ override fun useOn(context: UseOnContext): InteractionResult {
+ val level = context.level
+ val clickedPos = context.clickedPos
+ val clickedState = level.getBlockState(clickedPos)
+
+ val placePos = clickedPos.relative(context.clickedFace)
+
+ // Only place the cluster on TypeGemCore or TypeGemBlock
+ if (level.isEmptyBlock(placePos) || level.getBlockState(placePos).canBeReplaced()) {
+ val placePos = clickedPos.relative(context.clickedFace)
+
+ if (level.isEmptyBlock(placePos) || level.getBlockState(placePos).canBeReplaced()) {
+ val player = context.player
+ val itemInHand = context.itemInHand
+
+ if (!level.isClientSide) {
+ val facing = context.clickedFace
+
+ val shouldGrow = clickedState.block is TypeGemCoreBlock
+
+ val state = clusterBlock.defaultBlockState()
+ .setValue(DirectionalBlock.FACING, facing)
+ .setValue(TypeGemClusterBlock.STAGE, 0)
+ .setValue(TypeGemClusterBlock.SHOULD_GROW, shouldGrow)
+
+ level.setBlock(placePos, state, 3)
+
+ if (player == null || !player.isCreative) {
+ itemInHand.shrink(1)
+ }
+ }
+
+ return InteractionResult.sidedSuccess(level.isClientSide)
+ }
+ }
+
+ return InteractionResult.PASS
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/item/components/TMMoveComponent.kt b/common/src/main/kotlin/com/cobblemon/mod/common/item/components/TMMoveComponent.kt
new file mode 100644
index 0000000000..4a296b0f20
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/item/components/TMMoveComponent.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.item.components
+
+import com.cobblemon.mod.common.Cobblemon
+import com.cobblemon.mod.common.CobblemonItemComponents
+import com.cobblemon.mod.common.CobblemonItems
+import com.cobblemon.mod.common.api.moves.MoveTemplate
+import com.cobblemon.mod.common.api.moves.Moves
+import com.cobblemon.mod.common.api.types.ElementalTypes
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import io.netty.buffer.ByteBuf
+import net.minecraft.network.FriendlyByteBuf
+import net.minecraft.network.RegistryFriendlyByteBuf
+import net.minecraft.network.codec.ByteBufCodecs
+import net.minecraft.network.codec.StreamCodec
+import net.minecraft.world.item.ItemStack
+
+data class TMMoveComponent(val move: MoveTemplate) {
+ companion object {
+ // Codec to serialize/deserialize TMMoveComponent
+ val CODEC: Codec<TMMoveComponent> = RecordCodecBuilder.create { instance ->
+ instance.group(
+ Codec.STRING.fieldOf("move").forGetter { it.move.name } // Serialize the move's name
+ ).apply(instance) { moveName -> TMMoveComponent(Moves.getByNameOrDummy(moveName)) }
+ }
+
+ val PACKET_CODEC: StreamCodec<ByteBuf, TMMoveComponent> = ByteBufCodecs.fromCodec(CODEC)
+
+ fun getTMMove(stack: ItemStack): MoveTemplate? {
+ return stack.get(CobblemonItemComponents.TM_MOVE)?.move
+ }
+
+ fun setTMMove(stack: ItemStack, move: MoveTemplate): ItemStack {
+ stack.set(CobblemonItemComponents.TM_MOVE, TMMoveComponent(move))
+ return stack
+ }
+
+ fun removeTMMove(stack: ItemStack): ItemStack {
+ stack.remove(CobblemonItemComponents.TM_MOVE)
+ return stack
+ }
+
+ fun createStack(move: MoveTemplate?): ItemStack {
+ if (move == null) {
+ Cobblemon.LOGGER.error("Attempted to create TM stack with null move!")
+ throw IllegalArgumentException("MoveTemplate cannot be null in createStack.")
+ }
+ val stack = ItemStack(CobblemonItems.TECHNICAL_MACHINE)
+ return setTMMove(stack, move)
+ }
+
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/item/group/CobblemonItemGroups.kt b/common/src/main/kotlin/com/cobblemon/mod/common/item/group/CobblemonItemGroups.kt
index 0de0c62c56..56c004415d 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/item/group/CobblemonItemGroups.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/item/group/CobblemonItemGroups.kt
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.item.group
import com.cobblemon.mod.common.Cobblemon
+import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.CobblemonItemComponents
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.riding.stats.RidingStat
@@ -249,6 +250,7 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.PC)
entries.accept(CobblemonItems.HEALING_MACHINE)
entries.accept(CobblemonItems.PASTURE)
+ entries.accept(CobblemonItems.TM_MACHINE)
entries.accept(CobblemonItems.GILDED_CHEST)
entries.accept(CobblemonItems.YELLOW_GILDED_CHEST)
@@ -307,6 +309,26 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.BLACK_TUMBLESTONE_BLOCK)
entries.accept(CobblemonItems.SKY_TUMBLESTONE_BLOCK)
+ entries.accept(CobblemonItems.TYPE_GEM_CORE)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_NORMAL)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_FIRE)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_WATER)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_ELECTRIC)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_GRASS)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_ICE)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_FIGHTING)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_POISON)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_GROUND)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_FLYING)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_PSYCHIC)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_BUG)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_ROCK)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_GHOST)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_DRAGON)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_DARK)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_STEEL)
+ entries.accept(CobblemonItems.TYPE_GEM_BLOCK_FAIRY)
+
entries.accept(CobblemonItems.POLISHED_TUMBLESTONE)
entries.accept(CobblemonItems.POLISHED_TUMBLESTONE_STAIRS)
entries.accept(CobblemonItems.POLISHED_TUMBLESTONE_SLAB)
@@ -544,6 +566,8 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.ABILITY_CAPSULE)
entries.accept(CobblemonItems.ABILITY_PATCH)
+ entries.accept(CobblemonItems.BLANK_TM)
+
entries.accept(CobblemonItems.POKE_BAIT)
entries.accept(CobblemonItems.POKE_SNACK)
entries.accept(CobblemonItems.POKE_CAKE)
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/item/interactive/TechnicalMachineItem.kt b/common/src/main/kotlin/com/cobblemon/mod/common/item/interactive/TechnicalMachineItem.kt
new file mode 100644
index 0000000000..da250d983f
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/item/interactive/TechnicalMachineItem.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.item.interactive
+
+import com.cobblemon.mod.common.Cobblemon
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.interaction.PokemonEntityInteraction
+import com.cobblemon.mod.common.api.moves.BenchedMove
+import com.cobblemon.mod.common.api.text.gray
+import com.cobblemon.mod.common.api.text.green
+import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
+import com.cobblemon.mod.common.item.CobblemonItem
+import com.cobblemon.mod.common.item.components.TMMoveComponent
+import com.cobblemon.mod.common.util.lang
+import net.minecraft.network.chat.Component
+import net.minecraft.server.level.ServerPlayer
+import net.minecraft.sounds.SoundSource
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.TooltipFlag
+
+class TechnicalMachineItem(properties: Properties) : CobblemonItem(properties), PokemonEntityInteraction {
+ override val accepted: Set<PokemonEntityInteraction.Ownership> = setOf(PokemonEntityInteraction.Ownership.OWNER)
+
+ override fun processInteraction(player: ServerPlayer, entity: PokemonEntity, stack: ItemStack): Boolean {
+ val moveTemplate = TMMoveComponent.getTMMove(stack) ?: return false
+ val pokemon = entity.pokemon
+
+ val tmLearnableMoves = pokemon.form.moves.tmLearnableMoves()
+
+ if (moveTemplate !in tmLearnableMoves) {
+ player.displayClientMessage(lang("tms.cannot_learn", pokemon.getDisplayName(), moveTemplate.displayName), true)
+ return false
+ }
+ if (pokemon.moveSet.getMoveTemplates().contains(moveTemplate) || pokemon.allAccessibleMoves.contains(moveTemplate)) {
+ player.displayClientMessage(lang("tms.already_known", pokemon.getDisplayName(), moveTemplate.displayName), true)
+ return false
+ }
+
+ if (!player.isCreative && !Cobblemon.config.infiniteTmUses) stack.shrink(1)
+
+ if (pokemon.moveSet.hasSpace()) {
+ pokemon.moveSet.add(moveTemplate.create())
+ } else {
+ pokemon.benchedMoves.add(BenchedMove(moveTemplate, 0))
+ }
+
+ player.displayClientMessage(lang("tms.teach_move", pokemon.getDisplayName(), moveTemplate.displayName).green(), true)
+ player.level().playSound(null, player.blockPosition(), CobblemonSounds.TM_USE, SoundSource.PLAYERS, 1.0F, 1.0F)
+ entity.cry()
+ return true
+ }
+
+ override fun appendHoverText(stack: ItemStack, context: TooltipContext, tooltip: MutableList<Component>, tooltipFlag: TooltipFlag) {
+ val move = TMMoveComponent.getTMMove(stack)
+ val text = move?.displayName ?: lang("tms.unknown_move")
+ tooltip.add(text.gray())
+ super.appendHoverText(stack, context, tooltip, tooltipFlag)
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/settings/ServerSettingsPacket.kt b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/settings/ServerSettingsPacket.kt
index 620ca254d2..b88e2aadc6 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/settings/ServerSettingsPacket.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/settings/ServerSettingsPacket.kt
@@ -25,7 +25,8 @@ class ServerSettingsPacket internal constructor(
val displayEntityNameLabel: Boolean,
val maxPokemonLevel: Int,
val maxPokemonFriendship: Int,
- val maxDynamaxLevel: Int
+ val maxDynamaxLevel: Int,
+ val unlockAllMoveDexMovesByDefault: Boolean
) : NetworkPacket<ServerSettingsPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
@@ -35,9 +36,10 @@ class ServerSettingsPacket internal constructor(
buffer.writeInt(Cobblemon.config.maxPokemonLevel)
buffer.writeInt(Cobblemon.config.maxPokemonFriendship)
buffer.writeInt(Cobblemon.config.maxDynamaxLevel)
+ buffer.writeBoolean(Cobblemon.config.unlockAllMoveDexMovesByDefault)
}
companion object {
val ID = cobblemonResource("server_settings")
- fun decode(buffer: RegistryFriendlyByteBuf) = ServerSettingsPacket(buffer.readBoolean(), buffer.readBoolean(), buffer.readBoolean(), buffer.readInt(), buffer.readInt(), buffer.readInt())
+ fun decode(buffer: RegistryFriendlyByteBuf) = ServerSettingsPacket(buffer.readBoolean(), buffer.readBoolean(), buffer.readBoolean(), buffer.readInt(), buffer.readInt(), buffer.readInt(), buffer.readBoolean())
}
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/tms/TechnicalMachineRegistrySyncPacket.kt b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/tms/TechnicalMachineRegistrySyncPacket.kt
new file mode 100644
index 0000000000..aba543ada0
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/tms/TechnicalMachineRegistrySyncPacket.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.net.messages.client.tms
+
+import com.cobblemon.mod.common.Cobblemon
+import com.cobblemon.mod.common.api.moves.Moves
+import com.cobblemon.mod.common.api.tms.ObtainMethod
+import com.cobblemon.mod.common.api.tms.TechnicalMachine
+import com.cobblemon.mod.common.api.tms.TechnicalMachineRecipe
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.net.messages.client.data.DataRegistrySyncPacket
+import com.cobblemon.mod.common.util.cobblemonResource
+import net.minecraft.network.RegistryFriendlyByteBuf
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.world.item.crafting.Ingredient
+
+class TechnicalMachineRegistrySyncPacket(tms: List<TechnicalMachine>) : DataRegistrySyncPacket<TechnicalMachine, TechnicalMachineRegistrySyncPacket>(tms) {
+
+ companion object {
+ val ID = cobblemonResource("technical_machines")
+
+ fun decode(buffer: RegistryFriendlyByteBuf): TechnicalMachineRegistrySyncPacket {
+ return TechnicalMachineRegistrySyncPacket(emptyList()).apply { decodeBuffer(buffer) }
+ }
+ }
+
+ override val id = ID
+
+ override fun encodeEntry(buffer: RegistryFriendlyByteBuf, entry: TechnicalMachine) {
+ buffer.writeResourceLocation(entry.id)
+ buffer.writeUtf(entry.moveName.name)
+ if (entry.recipe != null) {
+ buffer.writeBoolean(true)
+ buffer.writeVarInt(entry.recipe.size)
+ entry.recipe.forEach {
+ Ingredient.CONTENTS_STREAM_CODEC.encode(buffer, it.ingredient)
+ buffer.writeVarInt(it.count)
+ }
+ } else {
+ buffer.writeBoolean(false)
+ }
+ buffer.writeVarInt(entry.obtainMethods.size)
+ entry.obtainMethods.forEach { it.writeToBuffer(buffer) }
+ buffer.writeUtf(entry.type)
+ }
+
+ override fun decodeEntry(buffer: RegistryFriendlyByteBuf): TechnicalMachine {
+ val id = buffer.readResourceLocation()
+ val moveName = buffer.readUtf()
+
+ val recipe = if (buffer.readBoolean()) {
+ val recipeCount = buffer.readVarInt()
+ List(recipeCount) {
+ val ingredient = Ingredient.CONTENTS_STREAM_CODEC.decode(buffer)
+ val count = buffer.readVarInt()
+ TechnicalMachineRecipe(ingredient, count)
+ }
+ } else null
+
+ val obtainMethods = List(buffer.readVarInt()) { ObtainMethod.readFromBuffer(buffer) }
+ val type = buffer.readUtf()
+
+ val moveTemplate = Moves.getByName(moveName)
+ if (moveTemplate == null) {
+ Cobblemon.LOGGER.error("MoveTemplate not found for move name: $moveName")
+ throw IllegalStateException("MoveTemplate not found for move name: $moveName")
+ }
+
+ return TechnicalMachine(moveTemplate, recipe, obtainMethods, type).apply { this.id = id }
+ }
+
+
+ override fun synchronizeDecoded(entries: Collection<TechnicalMachine>) {
+ TechnicalMachines.reload(entries.associateBy { it.id })
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/toast/ToastPacket.kt b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/toast/ToastPacket.kt
index 945a850967..eb95b519ca 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/toast/ToastPacket.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/toast/ToastPacket.kt
@@ -19,12 +19,13 @@ import net.minecraft.resources.ResourceLocation
class ToastPacket(
val title: Component,
val description: Component,
- val icon: ItemStack,
+ val icons: List<ItemStack>,
val frameTexture: ResourceLocation,
val progress: Float,
val progressColor: Int,
val uuid: UUID,
- val behaviour: Behaviour
+ val behaviour: Behaviour,
+ val durationMs: Long? = null
) : NetworkPacket<ToastPacket> {
override val id: ResourceLocation = ID
@@ -32,12 +33,15 @@ class ToastPacket(
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeText(this.title)
buffer.writeText(this.description)
- buffer.writeItemStack(this.icon)
+ buffer.writeCollection(this.icons) { _, icon ->
+ buffer.writeItemStack(icon)
+ }
buffer.writeIdentifier(this.frameTexture)
buffer.writeFloat(this.progress)
buffer.writeInt(this.progressColor)
buffer.writeUUID(this.uuid)
buffer.writeEnumConstant(this.behaviour)
+ if (durationMs != null) buffer.writeLong(durationMs)
}
companion object {
@@ -47,12 +51,13 @@ class ToastPacket(
fun decode(buffer: RegistryFriendlyByteBuf): ToastPacket = ToastPacket(
buffer.readText(),
buffer.readText(),
- buffer.readItemStack(),
+ buffer.readList { _ -> buffer.readItemStack() },
buffer.readIdentifier(),
buffer.readFloat(),
buffer.readInt(),
buffer.readUUID(),
- buffer.readEnumConstant(Behaviour::class.java)
+ buffer.readEnumConstant(Behaviour::class.java),
+ durationMs = if (buffer.readableBytes() >= 8) buffer.readLong() else null
)
}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/ui/OpenTMMPacket.kt b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/ui/OpenTMMPacket.kt
new file mode 100644
index 0000000000..74409a9095
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/ui/OpenTMMPacket.kt
@@ -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.net.messages.client.ui
+
+import com.cobblemon.mod.common.api.net.NetworkPacket
+import com.cobblemon.mod.common.util.cobblemonResource
+import net.minecraft.network.RegistryFriendlyByteBuf
+
+class OpenTMMPacket: NetworkPacket<OpenTMMPacket> {
+ override val id = cobblemonResource("open_tm")
+
+ override fun encode(buffer: RegistryFriendlyByteBuf) {
+
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/ui/SetActiveTMPacket.kt b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/ui/SetActiveTMPacket.kt
new file mode 100644
index 0000000000..8688c0f720
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/ui/SetActiveTMPacket.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.net.messages.client.ui
+
+import com.cobblemon.mod.common.api.net.NetworkPacket
+import com.cobblemon.mod.common.api.tms.TechnicalMachine
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.block.tmmachine.TMMachineBlock
+import com.cobblemon.mod.common.client.net.gui.SetActiveTMPacketHandler
+import com.cobblemon.mod.common.util.cobblemonResource
+import com.cobblemon.mod.common.item.interactive.TechnicalMachineItem
+import net.minecraft.network.RegistryFriendlyByteBuf
+
+/**
+ * Tells the server to set active [TechnicalMachineItem] using the [TMMachineBlock]
+ *
+ * Handled by [SetActiveTMPacketHandler]
+ */
+class SetActiveTMPacket(val tm: TechnicalMachine?): NetworkPacket<SetActiveTMPacket> {
+ override val id = ID
+
+ override fun encode(buffer: RegistryFriendlyByteBuf) {
+ buffer.writeNullable(tm) { _, v -> buffer.writeResourceLocation(v.id) }
+ }
+
+ companion object {
+ val ID = cobblemonResource("set_active_tm")
+
+ fun decode(buffer: RegistryFriendlyByteBuf) = SetActiveTMPacket(
+ TechnicalMachines.tmMap[buffer.readNullable { buffer.readResourceLocation() }]
+ )
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/ui/SetTMMachineContainerDataPacket.kt b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/ui/SetTMMachineContainerDataPacket.kt
new file mode 100644
index 0000000000..b10f6a0be2
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/client/ui/SetTMMachineContainerDataPacket.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.net.messages.client.ui
+
+import com.cobblemon.mod.common.api.net.NetworkPacket
+import com.cobblemon.mod.common.block.entity.TMMachineBlockEntity
+import com.cobblemon.mod.common.client.net.gui.SetTMMachineContainerDataPacketHandler
+import com.cobblemon.mod.common.util.cobblemonResource
+import net.minecraft.network.RegistryFriendlyByteBuf
+
+/**
+ * Tells the server to set container data within [TMMachineBlockEntity]
+ *
+ * Handled by [SetTMMachineContainerDataPacketHandler]
+ */
+class SetTMMachineContainerDataPacket(val dataSlotIndex: Int, val value: Int): NetworkPacket<SetTMMachineContainerDataPacket> {
+ override val id = ID
+
+ override fun encode(buffer: RegistryFriendlyByteBuf) {
+ buffer.writeInt(dataSlotIndex)
+ buffer.writeInt(value)
+ }
+
+ companion object {
+ val ID = cobblemonResource("tm_machine_set_container_data")
+
+ fun decode(buffer: RegistryFriendlyByteBuf) = SetTMMachineContainerDataPacket(
+ buffer.readInt(),
+ buffer.readInt()
+ )
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/server/block/TMMachineTeachMovePacket.kt b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/server/block/TMMachineTeachMovePacket.kt
new file mode 100644
index 0000000000..6ffec9e870
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/net/messages/server/block/TMMachineTeachMovePacket.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.net.messages.server.block
+
+import com.cobblemon.mod.common.api.moves.MoveTemplate
+import com.cobblemon.mod.common.api.moves.Moves
+import com.cobblemon.mod.common.api.net.NetworkPacket
+import com.cobblemon.mod.common.net.serverhandling.block.TMMachineTeachMoveHandler
+import com.cobblemon.mod.common.util.*
+import net.minecraft.network.RegistryFriendlyByteBuf
+import net.minecraft.world.item.ItemStack
+import java.util.UUID
+
+/**
+ * Handled by [TMMachineTeachMoveHandler].
+ */
+class TMMachineTeachMovePacket(val uuid: UUID, val heldStack: ItemStack, val moveTemplate: MoveTemplate?, val isParty: Boolean = true) : NetworkPacket<TMMachineTeachMovePacket> {
+ override val id = ID
+ override fun encode(buffer: RegistryFriendlyByteBuf) {
+ buffer.writeUUID(uuid)
+ buffer.writeNullable(moveTemplate) { _, v -> buffer.writeString(v.name) }
+ buffer.writeBoolean(isParty)
+ buffer.writeItemStack(heldStack)
+ }
+
+ companion object {
+ val ID = cobblemonResource("teach_move")
+ fun decode(buffer: RegistryFriendlyByteBuf): TMMachineTeachMovePacket {
+ val uuid = buffer.readUUID()
+ val moveName = buffer.readNullable { buffer.readString() }
+ val moveTemplate = if (moveName == null) null else Moves.getByName(moveName)
+ val isParty = buffer.readBoolean()
+ val heldStack = buffer.readItemStack()
+ return TMMachineTeachMovePacket(uuid, heldStack, moveTemplate, isParty)
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/net/serverhandling/block/TMMachineTeachMoveHandler.kt b/common/src/main/kotlin/com/cobblemon/mod/common/net/serverhandling/block/TMMachineTeachMoveHandler.kt
new file mode 100644
index 0000000000..2fb8ed55df
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/net/serverhandling/block/TMMachineTeachMoveHandler.kt
@@ -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.net.serverhandling.block
+
+import com.cobblemon.mod.common.Cobblemon
+import com.cobblemon.mod.common.CobblemonSounds
+import com.cobblemon.mod.common.api.moves.BenchedMove
+import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
+import com.cobblemon.mod.common.api.storage.PokemonStore
+import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
+import com.cobblemon.mod.common.item.components.TMMoveComponent
+import com.cobblemon.mod.common.net.messages.server.block.TMMachineTeachMovePacket
+import com.cobblemon.mod.common.util.party
+import com.cobblemon.mod.common.util.pc
+import net.minecraft.server.MinecraftServer
+import net.minecraft.server.level.ServerPlayer
+import net.minecraft.world.item.ItemStack
+
+object TMMachineTeachMoveHandler : ServerNetworkPacketHandler<TMMachineTeachMovePacket> {
+
+ override fun handle(packet: TMMachineTeachMovePacket, server: MinecraftServer, player: ServerPlayer) {
+ val pokemonStore: PokemonStore<*> = if (packet.isParty) player.party() else player.pc()
+ val pokemon = pokemonStore[packet.uuid] ?: return
+ val moveTemplate = packet.moveTemplate ?: return
+ val menu = player.containerMenu as? TMMachineMenu ?: return
+ val inventory = menu.inventory ?: return
+
+ if (ItemStack.isSameItemSameComponents(packet.heldStack, menu.carried) &&
+ moveTemplate == TMMoveComponent.getTMMove(packet.heldStack) &&
+ packet.heldStack.count == menu.carried.count &&
+ !pokemon.moveSet.getMoveTemplates().contains(moveTemplate) &&
+ !pokemon.allAccessibleMoves.contains(moveTemplate)
+ ) {
+ if (!player.isCreative && !Cobblemon.config.infiniteTmUses) menu.carried.shrink(1)
+
+ if (pokemon.moveSet.hasSpace()) {
+ pokemon.moveSet.add(moveTemplate.create())
+ } else {
+ pokemon.benchedMoves.add(BenchedMove(moveTemplate, 0))
+ }
+
+ inventory.setChanged()
+ menu.broadcastChanges()
+
+ menu.tmMachineEntity?.playSound(CobblemonSounds.TM_USE)
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/pokemon/Pokemon.kt b/common/src/main/kotlin/com/cobblemon/mod/common/pokemon/Pokemon.kt
index a29cf4cbc0..995210f724 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/pokemon/Pokemon.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/pokemon/Pokemon.kt
@@ -67,6 +67,8 @@ import com.cobblemon.mod.common.api.storage.StoreCoordinates
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
import com.cobblemon.mod.common.api.storage.pc.PCStore
+import com.cobblemon.mod.common.api.tms.TechnicalMachines
+import com.cobblemon.mod.common.api.tms.TMMoveManager
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.api.types.tera.TeraType
@@ -114,6 +116,7 @@ import com.cobblemon.mod.common.util.playSoundServer
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.util.setPositionSafely
import com.cobblemon.mod.common.util.toBlockPos
+import com.cobblemon.mod.common.util.tmList
import com.google.gson.JsonObject
import com.mojang.datafixers.util.Pair
import com.mojang.serialization.Codec
@@ -520,7 +523,11 @@ open class Pokemon : ShowdownIdentifiable {
* swap in moves they've used before at any time, while holding onto the remaining PP
* that they had last.
*/
- var benchedMoves = BenchedMoves().also { it.changeFunction = { onChange(BenchedMovesUpdatePacket({ this }, it)) }}
+ var benchedMoves = BenchedMoves().also { it.changeFunction = {
+ onChange(BenchedMovesUpdatePacket({ this }, it))
+ this.getOwnerUUID()?.let { it1 -> Cobblemon.playerDataManager.getTMData(it1)?.syncTMsFromPokemon(this) } // we want to make sure to update the players TM Data store when any moves are learned
+ }
+ }
internal set(value) {
val oldChangeFunction = field.changeFunction
field.changeFunction = {}
@@ -1564,8 +1571,7 @@ open class Pokemon : ShowdownIdentifiable {
originalTrainer = fakeTrainerName
}
- fun refreshOriginalTrainer()
- {
+ fun refreshOriginalTrainer() {
when (originalTrainerType)
{
OriginalTrainerType.PLAYER -> {
@@ -1584,8 +1590,7 @@ open class Pokemon : ShowdownIdentifiable {
}
}
- fun removeOriginalTrainer()
- {
+ fun removeOriginalTrainer() {
originalTrainer = null
originalTrainerType = OriginalTrainerType.NONE
originalTrainerName = null
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/pokemon/evolution/controller/ClientEvolutionController.kt b/common/src/main/kotlin/com/cobblemon/mod/common/pokemon/evolution/controller/ClientEvolutionController.kt
index 2d5271d4fd..668b9e28f9 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/pokemon/evolution/controller/ClientEvolutionController.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/pokemon/evolution/controller/ClientEvolutionController.kt
@@ -82,7 +82,7 @@ class ClientEvolutionController(
override fun asIntermediate(): Intermediate = Intermediate(this.evolutions)
fun sendPlayerNotification() {
- if (pokemon.heldItem?.item != CobblemonItems.EVERSTONE) {
+ if (pokemon.heldItem.item != CobblemonItems.EVERSTONE) {
PartyOverlayDataControl.pokemonGainedEvo(pokemon.uuid, 1)
}
}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/DefaultObtainMethod.kt b/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/DefaultObtainMethod.kt
new file mode 100644
index 0000000000..9cc2c19617
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/DefaultObtainMethod.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.tms.obtain
+
+import com.cobblemon.mod.common.api.tms.ObtainMethod
+import com.cobblemon.mod.common.util.cobblemonResource
+import net.minecraft.network.RegistryFriendlyByteBuf
+import net.minecraft.server.level.ServerPlayer
+
+/**
+ * An [ObtainMethod] that unlocks immediately by default.
+ * Used for TMs that aren't unlocked and are available immediately.
+ * TMs with this [ObtainMethod] will not display an unlock text.
+ *
+ * @author Plastered_Crab
+ */
+class DefaultObtainMethod : ObtainMethod {
+ override val passive = true
+
+ companion object {
+ val ID = cobblemonResource("default")
+
+ fun readFromBuffer(buffer: RegistryFriendlyByteBuf): DefaultObtainMethod {
+ return DefaultObtainMethod()
+ }
+ }
+
+ override fun matches(player: ServerPlayer) = true
+
+ override fun writeToBuffer(buffer: RegistryFriendlyByteBuf) {
+ buffer.writeUtf("cobblemon:default")
+ }
+}
+
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/PlayerHasAdvancementObtainMethod.kt b/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/PlayerHasAdvancementObtainMethod.kt
new file mode 100644
index 0000000000..c6e4da1a82
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/PlayerHasAdvancementObtainMethod.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.tms.obtain
+
+import com.cobblemon.mod.common.api.tms.ObtainMethod
+import com.cobblemon.mod.common.util.cobblemonResource
+import net.minecraft.network.RegistryFriendlyByteBuf
+import net.minecraft.server.level.ServerPlayer
+import net.minecraft.resources.ResourceLocation
+
+/**
+ * An [ObtainMethod] that triggers when the player has an advancement.
+ */
+class PlayerHasAdvancementObtainMethod(val advancement: ResourceLocation? = null) : ObtainMethod {
+
+ // todo we can leave this here in case we want to use it later.... or addons?
+ companion object {
+ val ID = cobblemonResource("advancement")
+
+ fun readFromBuffer(buffer: RegistryFriendlyByteBuf): PlayerHasAdvancementObtainMethod {
+ val hasAdvancement = buffer.readBoolean()
+ val advancement = if (hasAdvancement) buffer.readResourceLocation() else null
+ return PlayerHasAdvancementObtainMethod(advancement)
+ }
+ }
+
+ override val passive = true
+
+ override fun matches(player: ServerPlayer): Boolean {
+ if (advancement == null) return false
+ val advancementInstance = player.server.getAdvancements().get(advancement) ?: return false
+ val progress = player.advancements.getOrStartProgress(advancementInstance)
+ return progress != null && progress.isDone
+ }
+
+ override fun writeToBuffer(buffer: RegistryFriendlyByteBuf) {
+ buffer.writeUtf("cobblemon:advancement")
+ buffer.writeResourceLocation(advancement ?: ResourceLocation.tryParse("minecraft:empty"))
+ }
+}
+
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/PlayerYObtainMethod.kt b/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/PlayerYObtainMethod.kt
new file mode 100644
index 0000000000..9ad786ba94
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/PlayerYObtainMethod.kt
@@ -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.tms.obtain
+
+import com.cobblemon.mod.common.api.tms.ObtainMethod
+import com.cobblemon.mod.common.util.cobblemonResource
+import net.minecraft.network.RegistryFriendlyByteBuf
+import net.minecraft.server.level.ServerPlayer
+
+/**
+ * An [ObtainMethod] that checks the player's Y level.
+ *
+ * @param yLevel The Y level to check for.
+ * @param operator The operator to use. Can be "equals", "less", or "greater".
+ * @author whatsy
+ */
+class PlayerYObtainMethod(val yLevel: Int, val operator: String) : ObtainMethod {
+ override val passive = true
+
+ companion object {
+ val ID = cobblemonResource("y_level")
+
+ fun readFromBuffer(buffer: RegistryFriendlyByteBuf): PlayerYObtainMethod {
+ val yLevel = buffer.readVarInt()
+ val operator = buffer.readUtf()
+ return PlayerYObtainMethod(yLevel, operator)
+ }
+ }
+
+ override fun matches(player: ServerPlayer) = when (operator) {
+ "equals" -> player.y.toInt() == yLevel
+ "less" -> player.y.toInt() < yLevel
+ "greater" -> player.y.toInt() > yLevel
+ else -> false
+ }
+
+ override fun writeToBuffer(buffer: RegistryFriendlyByteBuf) {
+ buffer.writeUtf("cobblemon:y_level")
+ buffer.writeVarInt(yLevel)
+ buffer.writeUtf(operator)
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/UnlockableObtainMethod.kt b/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/UnlockableObtainMethod.kt
new file mode 100644
index 0000000000..00e5becb98
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/tms/obtain/UnlockableObtainMethod.kt
@@ -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.tms.obtain
+
+import com.cobblemon.mod.common.api.tms.ObtainMethod
+import com.cobblemon.mod.common.util.cobblemonResource
+import net.minecraft.network.RegistryFriendlyByteBuf
+import net.minecraft.server.level.ServerPlayer
+
+/**
+ * An [ObtainMethod] that triggers when you get the move on a Pokemon you own or insert into a Data Monitor
+ * @author Plastered_Crab
+ */
+class UnlockableObtainMethod : ObtainMethod {
+ override val passive = false
+
+ companion object {
+ val ID = cobblemonResource("unlockable")
+
+ fun readFromBuffer(buffer: RegistryFriendlyByteBuf): UnlockableObtainMethod {
+ return UnlockableObtainMethod()
+ }
+ }
+
+ override fun matches(player: ServerPlayer) = false
+
+ override fun writeToBuffer(buffer: RegistryFriendlyByteBuf) {
+ buffer.writeUtf("cobblemon:unlockable")
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/util/DataKeys.kt b/common/src/main/kotlin/com/cobblemon/mod/common/util/DataKeys.kt
index 7c56effca3..858f7e93a0 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/util/DataKeys.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/util/DataKeys.kt
@@ -211,6 +211,7 @@ object DataKeys {
const val HAS_CREATED_POKEMON = "HasCreatedPokemon"
const val FOSSIL_OWNER = "FossilOwner"
const val FOSSIL_STATE = "FossilState"
+ const val MONITOR_DISK = "MonitorDisk"
// Generic Block Entity
const val BLOCK_ENTITY_USER_AMOUNT = "userAmount"
@@ -266,4 +267,4 @@ object DataKeys {
// PokemonProperties
const val ELEMENTAL_TYPE = "ElementalType"
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/util/PlayerExtensions.kt b/common/src/main/kotlin/com/cobblemon/mod/common/util/PlayerExtensions.kt
index b0b44635db..b29f3e1e71 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/util/PlayerExtensions.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/util/PlayerExtensions.kt
@@ -60,6 +60,7 @@ import kotlin.math.sin
fun ServerPlayer.party() = Cobblemon.storage.getParty(this)
fun ServerPlayer.pc() = Cobblemon.storage.getPC(this)
fun ServerPlayer.pokedex() = Cobblemon.playerDataManager.getPokedexData(this)
+fun ServerPlayer.tmList() = Cobblemon.playerDataManager.getTMData(this)
val ServerPlayer.activeDialogue: ActiveDialogue?
get() = DialogueManager.activeDialogues[uuid]
val ServerPlayer.isInDialogue: Boolean
@@ -490,4 +491,4 @@ fun ServerPlayer.updateShoulderNbt(pokemon: Pokemon) {
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
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/util/adapters/IdentifierAdapter.kt b/common/src/main/kotlin/com/cobblemon/mod/common/util/adapters/IdentifierAdapter.kt
index 15d4912b86..1dc3d68101 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/util/adapters/IdentifierAdapter.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/util/adapters/IdentifierAdapter.kt
@@ -26,4 +26,4 @@ import net.minecraft.resources.ResourceLocation
object IdentifierAdapter : JsonSerializer<ResourceLocation>, JsonDeserializer<ResourceLocation> {
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext) = ResourceLocation.parse(json.asString)
override fun serialize(src: ResourceLocation, type: Type, ctx: JsonSerializationContext) = JsonPrimitive(src.toString())
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/util/adapters/TMObtainMethodAdapter.kt b/common/src/main/kotlin/com/cobblemon/mod/common/util/adapters/TMObtainMethodAdapter.kt
new file mode 100644
index 0000000000..a4b365aa6f
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/util/adapters/TMObtainMethodAdapter.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.util.adapters
+
+import com.cobblemon.mod.common.Cobblemon
+import com.cobblemon.mod.common.api.tms.ObtainMethod
+import com.cobblemon.mod.common.tms.obtain.DefaultObtainMethod
+import com.cobblemon.mod.common.tms.obtain.PlayerHasAdvancementObtainMethod
+import com.cobblemon.mod.common.tms.obtain.PlayerYObtainMethod
+import com.cobblemon.mod.common.tms.obtain.UnlockableObtainMethod
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import java.lang.reflect.Type
+import kotlin.reflect.KClass
+import net.minecraft.resources.ResourceLocation
+
+object TMObtainMethodAdapter : JsonSerializer<ObtainMethod>, JsonDeserializer<ObtainMethod> {
+ private const val VARIANT = "variant"
+ private val types = hashMapOf<String, KClass<out ObtainMethod>>()
+
+ init {
+ this.register(UnlockableObtainMethod::class, UnlockableObtainMethod.ID)
+ this.register(PlayerHasAdvancementObtainMethod::class, PlayerHasAdvancementObtainMethod.ID)
+ this.register(DefaultObtainMethod::class, DefaultObtainMethod.ID)
+ this.register(PlayerYObtainMethod::class, PlayerYObtainMethod.ID)
+ }
+
+ fun register(type: KClass<out ObtainMethod>, identifier: ResourceLocation) {
+ val existing = this.types.put(identifier.toString(), type)
+ if (existing != null) {
+ Cobblemon.LOGGER.debug(
+ "Replaced {} under ID {} with {} in the {}",
+ existing::class.qualifiedName,
+ identifier.toString(),
+ type.qualifiedName,
+ this::class.qualifiedName
+ )
+ }
+ }
+
+ override fun deserialize(jElement: JsonElement, type: Type, context: JsonDeserializationContext): ObtainMethod {
+ val json = jElement.asJsonObject
+ val variant = json.get(VARIANT).asString.lowercase()
+ val registeredType = this.types[variant]
+ ?: throw IllegalArgumentException("Cannot resolve type for variant $variant")
+ return context.deserialize(json, registeredType.java)
+ }
+
+ override fun serialize(method: ObtainMethod, type: Type, context: JsonSerializationContext): JsonElement {
+ val json = context.serialize(method).asJsonObject
+ val variant = this.types.entries.find { it.value == method::class }?.key
+ ?: throw IllegalArgumentException("Cannot resolve variant for type ${method::class.qualifiedName}")
+ json.addProperty(VARIANT, variant)
+ return json
+ }
+}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonConfiguredFeatures.kt b/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonConfiguredFeatures.kt
index 1307cc5c16..ece8e50c82 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonConfiguredFeatures.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonConfiguredFeatures.kt
@@ -43,6 +43,8 @@ object CobblemonConfiguredFeatures {
val REVIVAL_HERBS_KEY = of("revival_herb")
@JvmField
val BERRY_GROVE_KEY = of("berry_groves")
+ @JvmField
+ val TYPE_GEM_KEY = of("type_gems")
private fun of(id: String): ResourceKey<ConfiguredFeature<*, *>> = ResourceKey.create(Registries.CONFIGURED_FEATURE, cobblemonResource(id))
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonFeatures.kt b/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonFeatures.kt
index d03317a86c..96f66f488c 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonFeatures.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonFeatures.kt
@@ -28,4 +28,6 @@ object CobblemonFeatures : PlatformRegistry<Registry<Feature<*>>, ResourceKey<Re
val MINT_FEATURE = create("mint", MintBlockFeature())
@JvmField
val BERRY_GROVE_FEATURE = CobblemonFeatures.create("berry_grove", BerryGroveFeature())
+ @JvmField
+ val TYPE_GEM_FEATURE = create("type_gem", TypeGemFeature())
}
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonPlacedFeatures.kt b/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonPlacedFeatures.kt
index a432ab0756..6b930ce743 100644
--- a/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonPlacedFeatures.kt
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/CobblemonPlacedFeatures.kt
@@ -54,6 +54,9 @@ object CobblemonPlacedFeatures {
@JvmField
val BERRY_GROVE = of("berry_groves")
+ @JvmField
+ val TYPE_GEM = of("type_gems")
+
@JvmField
val SWAMP_GRAINS = of("swamp_grains")
@JvmField
@@ -71,6 +74,7 @@ object CobblemonPlacedFeatures {
Cobblemon.implementation.addFeatureToWorldGen(BIG_ROOT, GenerationStep.Decoration.VEGETAL_DECORATION, BiomeTags.IS_OVERWORLD)
Cobblemon.implementation.addFeatureToWorldGen(REVIVAL_HERB, GenerationStep.Decoration.VEGETAL_DECORATION, CobblemonBiomeTags.HAS_REVIVAL_HERBS)
Cobblemon.implementation.addFeatureToWorldGen(BERRY_GROVE, GenerationStep.Decoration.VEGETAL_DECORATION, BiomeTags.IS_OVERWORLD)
+ Cobblemon.implementation.addFeatureToWorldGen(TYPE_GEM, GenerationStep.Decoration.UNDERGROUND_ORES, BiomeTags.IS_OVERWORLD)
Cobblemon.implementation.addFeatureToWorldGen(SWAMP_GRAINS, GenerationStep.Decoration.VEGETAL_DECORATION, CobblemonBiomeTags.IS_SWAMP)
Cobblemon.implementation.addFeatureToWorldGen(PLAINS_GRAINS, GenerationStep.Decoration.VEGETAL_DECORATION, CobblemonBiomeTags.IS_PLAINS)
Cobblemon.implementation.addFeatureToWorldGen(GALARICA_NUTS, GenerationStep.Decoration.VEGETAL_DECORATION, CobblemonBiomeTags.IS_BEACH)
diff --git a/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/TypeGemFeature.kt b/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/TypeGemFeature.kt
new file mode 100644
index 0000000000..34f74246d4
--- /dev/null
+++ b/common/src/main/kotlin/com/cobblemon/mod/common/world/feature/TypeGemFeature.kt
@@ -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.world.feature
+
+import com.cobblemon.mod.common.CobblemonBlocks
+import com.cobblemon.mod.common.CobblemonBlocks.TYPE_GEM_CORE
+import com.cobblemon.mod.common.CobblemonBlocks.typeGemBlocks
+import com.cobblemon.mod.common.CobblemonBlocks.typeGemClusters
+import com.cobblemon.mod.common.block.TypeGemClusterBlock
+import com.cobblemon.mod.common.block.TypeGemCoreBlock
+import com.cobblemon.mod.common.util.random
+import net.minecraft.core.BlockPos
+import net.minecraft.core.Direction
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.tags.BlockTags
+import net.minecraft.world.level.WorldGenLevel
+import net.minecraft.world.level.block.Block.UPDATE_ALL
+import net.minecraft.world.level.levelgen.feature.Feature
+import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext
+import net.minecraft.world.level.levelgen.feature.configurations.BlockStateConfiguration
+
+class TypeGemFeature : Feature<BlockStateConfiguration>(BlockStateConfiguration.CODEC) {
+ override fun place(context: FeaturePlaceContext<BlockStateConfiguration?>): Boolean {
+ val worldGenLevel = context.level()
+ val random = context.random()
+ val origin = context.origin()
+
+ val originBlock = worldGenLevel.getBlockState(origin)
+ if (!originBlock.`is`(BlockTags.DEEPSLATE_ORE_REPLACEABLES)) {
+ return false
+ }
+
+ val neighborAirBlocks = getNeighborAirBlocks(worldGenLevel, origin)
+ if (neighborAirBlocks.isEmpty()) {
+ return false
+ }
+
+ val chosenPos = neighborAirBlocks[random.nextInt(neighborAirBlocks.size)]
+ val typeGemBlocks = typeGemBlocks().toList()
+ val (gemId, gemBlock) = typeGemBlocks[random.nextInt(typeGemBlocks.size)]
+ val gemState = gemBlock.defaultBlockState()
+ val coreState = TYPE_GEM_CORE.defaultBlockState()
+
+ // Place core and initial gem
+ worldGenLevel.setBlock(origin, coreState, UPDATE_ALL)
+ worldGenLevel.setBlock(chosenPos, gemState, UPDATE_ALL)
+
+ // Trigger forced growth with clusters
+ val coreBlock = coreState.block as TypeGemCoreBlock
+ coreBlock.forceGrow(worldGenLevel, origin, random, 0.5f)
+
+ // println("[TypeGemFeature] Placed ${gemId.path} at $chosenPos")
+ return true
+ }
+
+ fun getNeighborAirBlocks(worldGenLevel: WorldGenLevel, origin: BlockPos): List<BlockPos> {
+ val airBlocks = mutableListOf<BlockPos>()
+
+ for (direction: Direction in Direction.entries) {
+ val pos = origin.offset(direction.normal)
+ val blockState = worldGenLevel.getBlockState(pos)
+ if (blockState.isAir) {
+ airBlocks.add(pos)
+ }
+ }
+
+ return airBlocks
+ }
+}
diff --git a/fabric/src/main/kotlin/com/cobblemon/mod/fabric/data/generator/CobblemonFabricDataGenerator.kt b/fabric/src/main/kotlin/com/cobblemon/mod/fabric/data/generator/CobblemonFabricDataGenerator.kt
new file mode 100644
index 0000000000..1d7b02520b
--- /dev/null
+++ b/fabric/src/main/kotlin/com/cobblemon/mod/fabric/data/generator/CobblemonFabricDataGenerator.kt
@@ -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.fabric.data.generator
+
+import com.cobblemon.mod.common.Cobblemon
+import com.cobblemon.mod.fabric.data.generator.providers.TypeGemsLootTableProvider
+import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint
+import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator
+
+class CobblemonFabricDataGenerator : DataGeneratorEntrypoint {
+ override fun onInitializeDataGenerator(generator: FabricDataGenerator?) {
+ if (generator == null) {
+ Cobblemon.LOGGER.error("Fabric Data Generator couldn't initialize")
+ return
+ }
+
+ val pack = generator.createPack()
+
+ pack.addProvider(::TypeGemsLootTableProvider);
+ }
+}
\ No newline at end of file
diff --git a/fabric/src/main/kotlin/com/cobblemon/mod/fabric/data/generator/providers/TypeGemsLootTableProvider.kt b/fabric/src/main/kotlin/com/cobblemon/mod/fabric/data/generator/providers/TypeGemsLootTableProvider.kt
new file mode 100644
index 0000000000..6261a5bf05
--- /dev/null
+++ b/fabric/src/main/kotlin/com/cobblemon/mod/fabric/data/generator/providers/TypeGemsLootTableProvider.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.fabric.data.generator.providers
+
+import com.cobblemon.mod.common.CobblemonBlocks
+import com.cobblemon.mod.common.CobblemonItems
+import com.cobblemon.mod.common.block.TypeGemClusterBlock
+import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput
+import net.fabricmc.fabric.api.datagen.v1.provider.FabricBlockLootTableProvider
+import net.minecraft.advancements.critereon.*
+import net.minecraft.core.HolderLookup
+import net.minecraft.core.registries.Registries
+import net.minecraft.world.item.Item
+import net.minecraft.world.item.enchantment.Enchantments
+import net.minecraft.world.level.block.Block
+import net.minecraft.world.level.storage.loot.IntRange
+import net.minecraft.world.level.storage.loot.LootPool
+import net.minecraft.world.level.storage.loot.LootTable
+import net.minecraft.world.level.storage.loot.entries.AlternativesEntry
+import net.minecraft.world.level.storage.loot.entries.LootItem
+import net.minecraft.world.level.storage.loot.functions.ApplyBonusCount
+import net.minecraft.world.level.storage.loot.functions.ApplyExplosionDecay
+import net.minecraft.world.level.storage.loot.functions.LimitCount
+import net.minecraft.world.level.storage.loot.functions.SetItemCountFunction
+import net.minecraft.world.level.storage.loot.predicates.LootItemBlockStatePropertyCondition
+import net.minecraft.world.level.storage.loot.predicates.MatchTool
+import net.minecraft.world.level.storage.loot.providers.number.ConstantValue
+import net.minecraft.world.level.storage.loot.providers.number.UniformGenerator
+import java.util.concurrent.CompletableFuture
+
+class TypeGemsLootTableProvider(
+ dataOutput: FabricDataOutput,
+ val registryLookup: CompletableFuture<HolderLookup.Provider>
+) : FabricBlockLootTableProvider(dataOutput, registryLookup) {
+
+ override fun getName(): String? {
+ return "Type Gems Loot Tables"
+ }
+
+ override fun generate() {
+ val lookup = registryLookup.join()
+
+ val enchantments = lookup.lookupOrThrow(Registries.ENCHANTMENT)
+ val silkTouch = enchantments.getOrThrow(Enchantments.SILK_TOUCH)
+ val fortune = enchantments.getOrThrow(Enchantments.FORTUNE)
+
+ CobblemonBlocks.typeGemBlocks().values.forEach { block ->
+ val gem = BLOCK_TO_GEM[block] ?: return@forEach
+
+ add(
+ block,
+ LootTable.lootTable().withPool(
+ LootPool.lootPool().add(
+ AlternativesEntry.alternatives(
+ LootItem.lootTableItem(block).`when`(
+ MatchTool.toolMatches(
+ ItemPredicate.Builder.item()
+ .withSubPredicate(
+ ItemSubPredicates.ENCHANTMENTS,
+ ItemEnchantmentsPredicate.enchantments(
+ listOf(
+ EnchantmentPredicate(
+ silkTouch,
+ MinMaxBounds.Ints.atLeast(1)
+ )
+ )
+ ),
+ )
+ )
+ ),
+ LootItem.lootTableItem(gem)
+ .apply(
+ SetItemCountFunction.setCount(
+ UniformGenerator.between(1f, 2f)
+ )
+ )
+ .apply(
+ ApplyBonusCount.addOreBonusCount(fortune)
+ )
+ .apply(
+ LimitCount.limitCount(IntRange.upperBound(4))
+ )
+ .apply(
+ ApplyExplosionDecay.explosionDecay()
+ )
+ )
+ )
+ )
+ )
+ }
+
+ CobblemonBlocks.typeGemClusters().values.forEach { cluster ->
+ val gem = CLUSTER_TO_GEM[cluster] ?: return@forEach
+
+ add(
+ cluster,
+ LootTable.lootTable().withPool(
+ LootPool.lootPool().add(
+ AlternativesEntry.alternatives(
+ LootItem.lootTableItem(cluster).`when`(
+ MatchTool.toolMatches(
+ ItemPredicate.Builder.item()
+ .withSubPredicate(
+ ItemSubPredicates.ENCHANTMENTS,
+ ItemEnchantmentsPredicate.enchantments(
+ listOf(
+ EnchantmentPredicate(
+ silkTouch,
+ MinMaxBounds.Ints.atLeast(1)
+ )
+ )
+ ),
+ )
+ )
+ ),
+ LootItem.lootTableItem(gem).`when`(
+ LootItemBlockStatePropertyCondition
+ .hasBlockStateProperties(cluster)
+ .setProperties(
+ StatePropertiesPredicate.Builder.properties()
+ .hasProperty(TypeGemClusterBlock.STAGE, 3)
+ )
+ )
+ .apply(
+ SetItemCountFunction.setCount(
+ UniformGenerator.between(2f, 3f)
+ )
+ )
+ .apply(
+ ApplyBonusCount.addOreBonusCount(fortune)
+ )
+ .apply(
+ LimitCount.limitCount(IntRange.upperBound(4))
+ )
+ .apply(
+ ApplyExplosionDecay.explosionDecay()
+ ),
+ LootItem.lootTableItem(gem)
+ .apply(
+ SetItemCountFunction.setCount(
+ ConstantValue.exactly(1f)
+ )
+ )
+ .apply(
+ ApplyExplosionDecay.explosionDecay()
+ )
+ )
+ )
+ )
+ )
+ }
+ }
+
+ companion object {
+ val BLOCK_TO_GEM: Map<Block, Item> by lazy {
+ mapOf(
+ CobblemonBlocks.TYPE_GEM_BLOCK_NORMAL to CobblemonItems.NORMAL_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_FIRE to CobblemonItems.FIRE_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_WATER to CobblemonItems.WATER_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_GRASS to CobblemonItems.GRASS_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_ELECTRIC to CobblemonItems.ELECTRIC_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_ICE to CobblemonItems.ICE_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_FIGHTING to CobblemonItems.FIGHTING_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_POISON to CobblemonItems.POISON_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_GROUND to CobblemonItems.GROUND_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_FLYING to CobblemonItems.FLYING_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_PSYCHIC to CobblemonItems.PSYCHIC_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_BUG to CobblemonItems.BUG_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_ROCK to CobblemonItems.ROCK_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_GHOST to CobblemonItems.GHOST_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_DRAGON to CobblemonItems.DRAGON_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_DARK to CobblemonItems.DARK_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_STEEL to CobblemonItems.STEEL_GEM,
+ CobblemonBlocks.TYPE_GEM_BLOCK_FAIRY to CobblemonItems.FAIRY_GEM
+ )
+ }
+
+ val CLUSTER_TO_GEM: Map<Block, Item> by lazy {
+ mapOf(
+ CobblemonBlocks.TYPE_GEM_CLUSTER_NORMAL to CobblemonItems.NORMAL_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_FIRE to CobblemonItems.FIRE_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_WATER to CobblemonItems.WATER_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_GRASS to CobblemonItems.GRASS_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_ELECTRIC to CobblemonItems.ELECTRIC_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_ICE to CobblemonItems.ICE_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_FIGHTING to CobblemonItems.FIGHTING_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_POISON to CobblemonItems.POISON_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_GROUND to CobblemonItems.GROUND_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_FLYING to CobblemonItems.FLYING_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_PSYCHIC to CobblemonItems.PSYCHIC_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_BUG to CobblemonItems.BUG_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_ROCK to CobblemonItems.ROCK_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_GHOST to CobblemonItems.GHOST_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_DRAGON to CobblemonItems.DRAGON_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_DARK to CobblemonItems.DARK_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_STEEL to CobblemonItems.STEEL_GEM,
+ CobblemonBlocks.TYPE_GEM_CLUSTER_FAIRY to CobblemonItems.FAIRY_GEM
+ )
+ }
+ }
+
+}
\ No newline at end of file