Compare commits

..
Author SHA1 Message Date
jackowes cdb37c70f6 Fix up the steer force renderer and begin moving steering forces to an arbiter. Stepping stones to a future better system but its a start. 2026-04-08 23:26:32 -05:00
jackowes e052947d7a more obs avoid changes 2026-04-08 00:34:21 -05:00
jackowes 8ee18f7da4 dumb changes to go back to single block 2026-04-07 21:54:34 -05:00
jackowes 0ac418ec2c initial testing for object avoidance 2026-04-07 20:48:17 -05:00
jackowes 240804e201 Follow paths in smoothlier. Reynolds path following algorithm for steered entities 2026-04-05 23:17:55 -05:00
jackowes d3e1f6be9d Start of running ai 2026-04-05 14:33:43 -05:00
jackowes 6d7760ce30 Some cleanup and also a not so egregious implementation of steered behaviours with vanillas current path following logic. 2026-04-05 11:41:45 -05:00
jackowes 75e951191a squash merging the long way 2026-04-04 11:50:11 -05:00
jackowes 2157e9c09e - Bunch of moveControl testing and changes to steered behaviour logic.
- Added a few more tests mons to the new moveControl
2025-12-12 10:44:51 -06:00
jackowes 74ef1a61f9 - Fix pitch_tilt lerp to be 0.8 instead of 0.22
- Make it so players are affected by the custom push logic.
- Move base steering functions to extensions package
- Move the pokemonMoveControl over to steering logic temporarily
2025-12-05 18:26:49 -06:00
jackowes 23526824e8 - Attempted to smooth yrot turning
- weight influenced entity pushes
- make the predator seek its head to the entity position
2025-12-05 00:21:26 -06:00
jackowes 859e586503 - Pursue and avoidance tests
- Tried to hook up riding animations outside of riding but I think I forgot something.
2025-11-21 14:18:35 -06:00
jackowes a9b6c07ac9 - Add an avoid target 2025-11-20 00:35:55 -06:00
jackowes 82548e0624 - Fix to steering wander. 2025-11-19 23:56:52 -06:00
jackowes 552a405e2e - attempt to remedy line jumping 2025-11-16 09:35:20 -06:00
jackowes 08918956db Merge branch 'main' into manfred-herd-test
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/CobblemonActivities.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/CobblemonMemories.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/CobblemonNetwork.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/CobblemonSensors.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/entity/pokemon/PokemonEntity.kt
#	common/src/main/resources/assets/cobblemon/lang/en_us.json
2025-11-15 15:29:50 -06:00
jackowes c139a1fea9 Bunch of improvements to the debugRenderer. Still a one frame relocation of the render position 2025-09-19 00:42:02 -05:00
jackowes 040ae26f22 Merge branch 'main' into manfred-herd-test
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/behaviour/types/liquid/DolphinBehaviour.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/entity/ai/FollowHerdLeaderTask.kt
2025-09-18 16:17:02 -05:00
jackowes 4c9b4a4f1f Add rudimentary obstacle avoidance debug renderer. 2025-09-15 01:22:57 -05:00
jackowes 7d34f83a8f some odd boid changes that might need to be changed back? cohesion not strong enough? 2025-09-13 10:12:15 -05:00
jackowes 8d89a37d93 unfinised wander 2025-09-09 21:29:21 -05:00
jackowes 65c5748c77 boids improvements 2025-09-08 20:18:54 -05:00
jackowes d4821c15bd full boids alg 1.0 2025-09-07 20:12:45 -05:00
jackowes c753ca54f1 start of boid nonsense. 2025-08-29 16:29:55 -05:00
jackowes ef581d1aa4 Merge remote-tracking branch 'origin/implement_pokemon' into manfred-herd-test 2025-08-28 13:08:07 -05:00
jackowes 1f4224b029 some random herd tests 2025-08-28 13:05:03 -05:00
127 changed files with 2499 additions and 8160 deletions

No files matched your search

-20
View File
@@ -1,20 +0,0 @@
# Changelog
## [1.8.0 (MONTH Xth, 2026)](#1-8-0)
### Additions
- Added a new `cobblemon:structure`, which is a feature without any restrictions allowing the Builders to place certain structures in much more specific manner.
### Changes
### Fixes
- Fixed rare Pokémon being far too likely to spawn multiple times in a single spawn cycle. The dev told me that the exact bug is too complicated to explain.
### Developer
- Tweaked the spawning selector interface to take a bucket function rather than a bucket directly.
### Molang & Datapacks
### Particles
### Localization
- Updated translations for:
-16
View File
@@ -1,16 +0,0 @@
### Additions
### Changes
- Wild Pokémon now reliably have at least one offensive move, if they can have offensive moves at that level.
### Fixes
### Developer
### Molang & Datapacks
- Added `moveset_builders` datapack folder for different styles of movesets.
- Added `party_pools` datapack folder for defining groups of Pokémon for the purposes of complex NPC party definitions.
- Added `party_compositions` datapack folder for arranging parties on NPCs that rely on party pools.
- Added `composed_pool` NPC party provider type which combines a party pool with a party composition to create more complex NPC parties.
### Particles
+7
View File
@@ -2,6 +2,7 @@
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
### Additions
- Added config options to award experience to fainted Pokémon for opponents they helped defeat (`awardExperienceToFaintedPokemon`) and to still award experience for defeated enemies even after battle loss/forfeit (`awardExperienceOnBattleLoss`)
- Party Overlay Exp Gained Animation will now display the amount of EXP the Pokémon gained
- Added a toast to notify players of how they can scroll through their party. Did you know you can hold R and scroll instead of using arrow keys??!
- Added new layer property that makes a layer scroll in a direction. "scrolling": {"speedU": 0.1, "speedV": 0.1}
@@ -37,15 +38,21 @@
- [Various UFO Ride Improvements](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2156)
- Fixed NeoForge brewing stand unexpectedly crashing users when shift clicking items within it's menu
- Fixed NPCs switching pokemon very often during battles
- Fixed Campfire pot causing crashes under certain circumstances
- Fixed Type Gems not being consumed upon use
- Fixed Ancient Pokeball's to their respective modifiers
- Fixed a Server warning when recalling Pokemon
### Developer
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters
- Added `BattleFleeAttemptEvent` fired from `PokemonBattle.checkFleeAttempt`, allowing developers to intercept and control flee attempts
### Molang & Datapacks
- q.item.is_enchanted()
- q.item.has_enchantment(minecraft:sharpness, 3)
- Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions
- Added `marks`, `has_mark`, & `remove_marks` as available Molang functions for pokemon
### Localization
- Updated translations for:
+1 -1
View File
@@ -55,7 +55,7 @@ dependencies {
// modCompileOnly(libs.flywheelFabric)
// Showdown
modCompileOnly(libs.graal)
modCompileOnly(libs.graal.core)
// Data Storage
modCompileOnly(libs.bundles.mongo)
@@ -101,7 +101,7 @@ public abstract class ServerEntityMixin {
cobblemon$broadcast(new ClientboundUpdateDriverInputPacket(driverInput, entity.getId()));
}
private void cobblemon$broadcast(NetworkPacket<?> packet) {
public void cobblemon$broadcast(NetworkPacket<?> packet) {
if (!(entity.level() instanceof ServerLevel level)) return;
ChunkMap chunkMap = level.getChunkSource().chunkMap;
Int2ObjectMap<?> entityMap = ((ChunkMapAccessor) chunkMap).getEntityMap();
@@ -28,6 +28,8 @@ object CobblemonActivities {
@JvmField
val POKEMON_HERD = register(Activity("pokemon_herd"))
@JvmField
val POKEMON_BOIDS = register(Activity("pokemon_boid"))
@JvmField
val POKEMON_POLLINATION = register(Activity("pokemon_pollination"))
@JvmStatic
@@ -74,8 +74,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val NPC_EDITOR = create("npc_editor", CobblemonItem(Item.Properties().stacksTo(1)))
@JvmField
val POKEEDIT_WAND = create("pokeedit_wand", PokeEditWandItem())
@JvmField
val pokeBalls = mutableListOf<PokeBallItem>()
@@ -21,6 +21,7 @@ import net.minecraft.core.UUIDUtil
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import java.util.*
import net.minecraft.world.entity.player.Player
object CobblemonMemories {
val memories = mutableMapOf<String, MemoryModuleType<*>>()
@@ -92,6 +93,9 @@ object CobblemonMemories {
val DISABLE_WALK_TO_WANTED_ITEM = register<Boolean>("disable_walk_to_wanted_item")
@JvmField
val TIME_TRYING_TO_REACH_WANTED_ITEM = register<Int>("time_trying_to_wanted_item")
/** Is the nearest player holding a diamond for debugging purposes? */
@JvmField
val DIAMOND_DEBUGGER = register<Player>("diamond_debugger")
@JvmStatic
fun <U> register(id: String, codec: Codec<U>): MemoryModuleType<U> {
@@ -1,55 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.moves.MoveSelector
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.MovesetBuilderAdapter
import com.cobblemon.mod.common.util.adapters.StringToObjectAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonMovesetBuilders : JsonDataRegistry<MovesetBuilder> {
override val gson: Gson = GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderAdapter)
.registerTypeAdapter(MoveSelector::class.java, StringToObjectAdapter(MoveSelector.selectors))
.create()
override val typeToken = TypeToken.get(MovesetBuilder::class.java)
override val resourcePath = "moveset_builders"
override val id: ResourceLocation = cobblemonResource("moveset_builders")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<CobblemonMovesetBuilders>()
val movesetBuilders = mutableMapOf<ResourceLocation, MovesetBuilder>()
fun getOrThrow(id: ResourceLocation): MovesetBuilder {
return movesetBuilders[id]
?: throw IllegalArgumentException("Unknown MovesetBuilder id: $id")
}
override fun sync(player: ServerPlayer) { /* These don't sync. But what if they did? Nah probably shouldn't sync. */ }
override fun reload(data: Map<ResourceLocation, MovesetBuilder>) {
movesetBuilders.clear()
data.forEach { (id, value) -> value.id = id }
movesetBuilders.putAll(data)
observable.emit(this)
}
}
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
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.debug.S2CUpdateObsAvoidHandler
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.PokeSnackBlockParticlesHandler
@@ -80,6 +81,7 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallba
import com.cobblemon.mod.common.net.messages.client.cooking.SeasoningRegistrySyncPacket
import com.cobblemon.mod.common.net.messages.client.cooking.ToggleCookingPotLidPacket
import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
@@ -170,7 +172,6 @@ import com.cobblemon.mod.common.net.messages.server.pasture.UnpastureAllPokemonP
import com.cobblemon.mod.common.net.messages.server.pasture.UnpasturePokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.StartScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.interact.InteractPokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetActiveMarkPacket
@@ -228,7 +229,6 @@ import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHa
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandler
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.pokeedit.PokeEditActionHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.ServerboundUpdateRidingStateHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetActiveMarkHandler
@@ -255,7 +255,11 @@ import com.cobblemon.mod.common.net.serverhandling.trade.OfferTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.PerformTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.UpdateTradeOfferHandler
import com.cobblemon.mod.common.util.server
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Entity
import com.cobblemon.mod.common.mixin.accessor.ChunkMapAccessor
import com.cobblemon.mod.common.mixin.accessor.TrackedEntityAccessor
/**
* Registers Cobblemon network packets.
@@ -278,6 +282,19 @@ object CobblemonNetwork {
fun sendToAllPlayers(packet: NetworkPacket<*>) = sendPacketToPlayers(server()!!.playerList.players, packet)
@JvmStatic
fun sendPacketToPlayers(players: Iterable<ServerPlayer>, packet: NetworkPacket<*>) = players.forEach { sendPacketToPlayer(it, packet) }
fun sendPacketToTrackingPlayers(entity: Entity, packet: NetworkPacket<*>) {
val level = entity.level() as? ServerLevel ?: return
val chunkMap = level.chunkSource.chunkMap
val entityTracker = (chunkMap as ChunkMapAccessor).entityMap[entity.id]
if (entityTracker is TrackedEntityAccessor) {
entityTracker.seenBy.forEach { connection ->
sendPacketToPlayer(connection.player, packet)
}
}
}
val s2cPayloads = generateS2CPacketInfoList()
val c2sPayloads = generateC2SPacketInfoList()
@@ -485,6 +502,7 @@ object CobblemonNetwork {
// Debug / cheats
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
list.add(PacketRegisterInfo(ClientboundUpdateObsAvoidPacket.ID, ClientboundUpdateObsAvoidPacket::decode, S2CUpdateObsAvoidHandler))
// Riding
list.add(PacketRegisterInfo(ClientboundUpdateRidingStatePacket.ID, ClientboundUpdateRidingStatePacket::decode, ClientboundUpdateRidingStateHandler))
@@ -563,9 +581,6 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(StartScanningPacket.ID, StartScanningPacket::decode, StartScanningHandler))
list.add(PacketRegisterInfo(FinishScanningPacket.ID, FinishScanningPacket::decode, FinishScanningHandler))
// PokeEdit actions
list.add(PacketRegisterInfo(PokeEditActionPacket.ID, PokeEditActionPacket::decode, PokeEditActionHandler))
// Pasture
list.add(PacketRegisterInfo(PasturePokemonPacket.ID, PasturePokemonPacket::decode, PasturePokemonHandler))
list.add(PacketRegisterInfo(UnpasturePokemonPacket.ID, UnpasturePokemonPacket::decode, UnpasturePokemonHandler))
@@ -624,4 +639,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -1,47 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.npc.PartyComposition
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonPartyCompositions : JsonDataRegistry<PartyComposition> {
override val id: ResourceLocation = cobblemonResource("party_compositions")
override val type = PackType.SERVER_DATA
override val typeToken: TypeToken<PartyComposition> = TypeToken.get(PartyComposition::class.java)
override val resourcePath: String = "party_compositions"
override val observable = SimpleObservable<CobblemonPartyCompositions>()
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.setPrettyPrinting()
.create()
val partyCompositions = mutableMapOf<ResourceLocation, PartyComposition>()
override fun sync(player: ServerPlayer) {
// probably worth syncing which exist, yeah. the deeper details probably not. I'll get to this eventually.
}
override fun reload(data: Map<ResourceLocation, PartyComposition>) {
data.entries.forEach { it.value.id = it.key }
partyCompositions.clear()
partyCompositions.putAll(data)
observable.emit(this)
Cobblemon.LOGGER.info("Loaded ${data.size} party compositions.")
}
}
@@ -1,60 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.npc.PartyPool
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
import com.cobblemon.mod.common.util.adapters.MovesetBuilderReferenceAdapter
import com.cobblemon.mod.common.util.adapters.PokemonPropertiesAdapter
import com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object CobblemonPartyPools : JsonDataRegistry<PartyPool> {
override val id: ResourceLocation = cobblemonResource("party_pools")
override val type = PackType.SERVER_DATA
override val typeToken: TypeToken<PartyPool> = TypeToken.get(PartyPool::class.java)
override val resourcePath: String = "party_pools"
override val observable = SimpleObservable<CobblemonPartyPools>()
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(PokemonProperties::class.java, PokemonPropertiesAdapter(saveLong = false))
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapter(ScriptableIntRange::class.java, ScriptableIntRangeAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.setPrettyPrinting()
.create()
val partyPools = mutableMapOf<ResourceLocation, PartyPool>()
override fun sync(player: ServerPlayer) {
// probably worth syncing. Knowing the details might help exploit the server? lot of effort for little though.
}
override fun reload(data: Map<ResourceLocation, PartyPool>) {
data.entries.forEach { it.value.id = it.key }
partyPools.clear()
partyPools.putAll(data)
observable.emit(this)
}
}
@@ -49,6 +49,9 @@ object CobblemonSensors {
@JvmField
val NEARBY_SWEET_BERRY_BUSH = register("nearby_sweet_berry_bush", ::SweetBerryBushSensor)
@JvmField
val PLAYER_DEBUGGING = register("player_debugging", ::PlayerDebuggingSensor)
@JvmField
val POKEMON_NEARBY_WANTED_ITEM = register("pokemon_nearby_wanted_item", ::PokemonItemSensor)
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonSensors
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.ai.WrapperLivingEntityTask
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.tasks.WakeUpTask
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
class BoidsTaskConfig : SingleTaskConfig {
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
if (entity !is PokemonEntity) {
return null
}
behaviourConfigurationContext.addMemories(CobblemonMemories.POKEMON_DROWSY, CobblemonMemories.POKEMON_SLEEPING)
behaviourConfigurationContext.addSensors(CobblemonSensors.POKEMON_DROWSY)
return WrapperLivingEntityTask(WakeUpTask.create(), PokemonEntity::class.java)
}
}
@@ -0,0 +1,44 @@
/*
* 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.ai.config.task
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.BoidsMoveControl
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
class SwitchFromBoidTaskConfig : SingleTaskConfig {
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
behaviourConfigurationContext.addMemories(CobblemonMemories.DIAMOND_DEBUGGER)
return BehaviorBuilder.create {
it.group(
it.absent(CobblemonMemories.DIAMOND_DEBUGGER)
).apply(it) { _ ->
Trigger { world, entity, time ->
if(entity !is PokemonEntity) return@Trigger false
if (entity.moveControl !is PokemonMoveControl) {
entity.moveControl = PokemonMoveControl(entity)
}
entity.brain.updateActivityFromSchedule(world.dayTime, world.gameTime)
return@Trigger true
}
}
}
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.ai.config.task
import com.cobblemon.mod.common.CobblemonActivities
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonSensors
import com.cobblemon.mod.common.api.ai.BehaviourConfigurationContext
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.BoidsMoveControl
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.behavior.BehaviorControl
import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
import net.minecraft.world.entity.ai.behavior.declarative.Trigger
import net.minecraft.world.entity.player.Player
class SwitchToBoidTaskConfig : SingleTaskConfig {
override fun getVariables(entity: LivingEntity, behaviourConfigurationContext: BehaviourConfigurationContext) = emptyList<MoLangConfigVariable>()
override fun createTask(
entity: LivingEntity,
behaviourConfigurationContext: BehaviourConfigurationContext
): BehaviorControl<in LivingEntity>? {
behaviourConfigurationContext.addMemories(CobblemonMemories.DIAMOND_DEBUGGER)
behaviourConfigurationContext.addSensors(CobblemonSensors.PLAYER_DEBUGGING)
return BehaviorBuilder.create {
it.group(
it.present(CobblemonMemories.DIAMOND_DEBUGGER)
).apply(it) { _ ->
Trigger { world, entity, time ->
if(entity !is PokemonEntity) return@Trigger false
val player = entity.brain.getMemory(CobblemonMemories.DIAMOND_DEBUGGER).orElse(null)
if (entity.moveControl !is BoidsMoveControl && player is LivingEntity) {
entity.moveControl = BoidsMoveControl(entity, player)
} else {
return@Trigger false
}
entity.brain.setActiveActivityIfPossible(CobblemonActivities.POKEMON_BOIDS)
// SpawnSnowstormEntityParticlePacket(
// cobblemonResource("evo_particles"),
// entity.id,
// listOf("shiny_particles", "middle")).sendToPlayersAround(
// entity.x,
// entity.y,
// entity.z,
// 64.0,
// entity.level().dimension()
// )
return@Trigger true
}
}
}
}
}
@@ -124,6 +124,8 @@ interface TaskConfig {
cobblemonResource("target_entity") to TargetEntityTaskConfig::class.java,
cobblemonResource("memory_aspect") to MemoryAspectTaskConfig::class.java,
cobblemonResource("activity_change") to ActivityChangeTaskConfig::class.java,
cobblemonResource("switch_from_boid") to SwitchFromBoidTaskConfig::class.java,
cobblemonResource("switch_to_boid") to SwitchToBoidTaskConfig::class.java,
)
}
@@ -22,18 +22,21 @@ import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.api.battles.model.actor.FleeableBattleActor
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pokemon.stats.BattleEvSource
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.white
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.BattleCaptureAction
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.battles.ForfeitActionResponse
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.dispatch.BattleDispatch
@@ -48,7 +51,9 @@ import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
import com.cobblemon.mod.common.pokemon.evolution.progress.DefeatEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
import com.cobblemon.mod.common.pokemon.requirements.DefeatRequirement
@@ -137,6 +142,9 @@ open class PokemonBattle(
val captureActions = mutableListOf<BattleCaptureAction>()
val majorBattleActions = hashMapOf<UUID, BattleMessage>()
/** Monotonically increasing counter to track faint ordering within this battle. */
var faintCounter = 0
val minorBattleActions = hashMapOf<UUID, BattleMessage>()
val contextManager = ContextManager()
@@ -244,12 +252,23 @@ open class PokemonBattle(
fun end() {
ended = true
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
this.actors.forEach { actor ->
val faintedPokemons = actor.pokemonList.filter { it.health <= 0 }
actor.getSide().getOppositeSide().actors.forEach { opponent ->
val opponentNonFaintedPokemons = opponent.pokemonList.filter { it.health > 0 }
val opponentPokemons = if (awardToFainted) opponent.pokemonList else opponent.pokemonList.filter { it.health > 0 }
faintedPokemons.forEach { faintedPokemon ->
for (opponentPokemon in opponentNonFaintedPokemons) {
for (opponentPokemon in opponentPokemons) {
// Faint-order check: if awarding XP to fainted pokemon, only award for enemies that fainted before this pokemon did
if (awardToFainted) {
val enemyFaintedAt = faintedPokemon.faintedAt ?: continue
val opponentFaintedAt = opponentPokemon.faintedAt
if (opponentFaintedAt != null && opponentFaintedAt <= enemyFaintedAt) {
continue
}
}
val facedFainted = opponentPokemon.facedOpponents.contains(faintedPokemon)
val pokemon = opponentPokemon.effectedPokemon
if (facedFainted) {
@@ -270,7 +289,8 @@ open class PokemonBattle(
else -> continue
}
val experience = Cobblemon.experienceCalculator.calculate(opponentPokemon, faintedPokemon, multiplier)
if (experience > 0 && actor.pokemonList.all { it.health <= 0 }) {
val enemyTeamWiped = actor.pokemonList.all { it.health <= 0 }
if (experience > 0 && (enemyTeamWiped || awardOnLoss)) {
opponent.awardExperience(opponentPokemon, experience)
}
Cobblemon.evYieldCalculator.calculate(opponentPokemon, faintedPokemon).forEach { (stat, amount) ->
@@ -505,6 +525,7 @@ open class PokemonBattle(
fun checkForInputDispatch() {
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
if (checkFleeAttempt()) return
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
if (readyToInput && captureActions.isEmpty()) {
actors.filter { it.responses.isNotEmpty() }.forEach { it.writeShowdownResponse() }
@@ -512,6 +533,20 @@ open class PokemonBattle(
}
}
private fun checkFleeAttempt(): Boolean {
val runner = actors.find { it.responses.any { it is FleeAttemptActionResponse } } as? PlayerBattleActor ?: return false
CobblemonEvents.BATTLE_FLEE_ATTEMPT.post(BattleFleeAttemptEvent(this, runner))
runner.responses.clear()
runner.mustChoose = true
runner.request?.let { runner.sendUpdate(BattleQueueRequestPacket(it)) }
runner.sendUpdate(BattleMakeChoicePacket())
runner.entity?.sendSystemMessage(battleLang("run_prompt").white())
return true
}
/** Forces Showdown to end the battle when a [BattleActor] chooses to forfeit. */
private fun checkForfeit(): Boolean {
val forfeit = actors.find { it.responses.any { it is ForfeitActionResponse } }
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.events
import com.cobblemon.mod.common.api.events.battles.BattleFaintedEvent
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
import com.cobblemon.mod.common.api.events.battles.BattleStartedEvent
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
@@ -110,6 +111,9 @@ object CobblemonEvents {
@JvmField
val BATTLE_STARTED_POST = EventObservable<BattleStartedEvent.Post>()
@JvmField
val BATTLE_FLEE_ATTEMPT = EventObservable<BattleFleeAttemptEvent>()
@JvmField
val BATTLE_FLED = EventObservable<BattleFledEvent>()
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.util.asArrayValue
/**
* Event fired when a [PokemonBattle] is fled by a [PlayerBattleActor].
*
* @author saultsc
* @since March 31th 2026
*/
data class BattleFleeAttemptEvent(
override val battle: PokemonBattle,
val player: PlayerBattleActor
) : BattleEvent {
val context = mutableMapOf<String, MoValue>(
"battle" to battle.struct,
"players" to battle.actors.filter { it.type == ActorType.PLAYER }.asArrayValue { it.struct },
"npcs" to battle.actors.filter { it.type == ActorType.NPC }.asArrayValue { it.struct },
"wild_pokemon" to battle.actors.filter { it.type == ActorType.WILD }.asArrayValue { it.struct }
)
}
@@ -1842,6 +1842,30 @@ object MoLangFunctions {
map.put("remove_cosmetic_item") { _ ->
pokemon.removeCosmeticItem()
}
map.put("marks") { _ -> pokemon.marks.asArrayValue { StringValue(it.identifier.toString()) } }
map.put("has_mark") { params ->
var hasMark = false
val identifier = params.getString(0).asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) {
hasMark = pokemon.marks.contains(mark)
}
DoubleValue(hasMark)
}
map.put("remove_marks") { params ->
var removedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) {
pokemon.exchangeMark(mark, false)
removedMark = true
}
}
DoubleValue(removedMark)
}
map.put("add_marks") { params ->
var appliedMark = false
for (param in params.params) {
@@ -1941,7 +1965,8 @@ object MoLangFunctions {
return@put DoubleValue(if (pokemon.addRideBoost(stat, value)) 1.0 else 0.0)
}
map.put("initialize_moveset") { params ->
pokemon.initializeMovesetFromDefault()
val preferLatest = params.getBooleanOrNull(0) ?: true
pokemon.initializeMoveset(preferLatest)
return@put DoubleValue.ONE
}
map.put("validate_moveset") { params ->
@@ -2034,9 +2059,13 @@ object MoLangFunctions {
}
}
map.put("is_sprinting") {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions.get("sprinting")?.apply(MoParams.EMPTY) as? MoValue ?: DoubleValue.ZERO
if (pokemonEntity.hasControllingPassenger()) { // If its being ridden then the riding state sets the animation
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions.get("sprinting")?.apply(MoParams.EMPTY) as? MoValue ?: DoubleValue.ZERO
}
} else {
pokemonEntity.isPokemonSprinting
}
}
map.put("is_drifting") {
@@ -1,57 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
/**
* This is all you need tbh. A moveset builder that picks moves from up to 4 slots of move selectors.
*
* @author Hiroku
* @since December 5th, 2025
*/
class DefaultMovesetBuilder : MovesetBuilder {
companion object {
val TYPE = cobblemonResource("default")
}
override val type: ResourceLocation = TYPE
override var id: ResourceLocation = cobblemonResource("temp") // replaced on load.
val slot1: List<MoveSelector> = listOf()
val slot2: List<MoveSelector> = listOf()
val slot3: List<MoveSelector> = listOf()
val slot4: List<MoveSelector> = listOf()
override fun build(form: FormData, level: Int): MoveSet {
val chosenMoves = mutableSetOf<MoveTemplate>()
tryAddMoveFromSlot(form, level, slot1, chosenMoves)
tryAddMoveFromSlot(form, level, slot2, chosenMoves)
tryAddMoveFromSlot(form, level, slot3, chosenMoves)
tryAddMoveFromSlot(form, level, slot4, chosenMoves)
if (chosenMoves.isEmpty()) {
chosenMoves.add(Moves.getExceptional())
}
val moveset = MoveSet()
chosenMoves.map(MoveTemplate::create).forEach(moveset::add)
return moveset
}
fun tryAddMoveFromSlot(form: FormData, level: Int, selectors: List<MoveSelector>, chosenMoves: MutableSet<MoveTemplate>) {
for (selector in selectors) {
val move = selector(form, form.moves, level, chosenMoves)
if (move != null) {
chosenMoves.add(move)
return
}
}
}
}
@@ -1,148 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.pokemon.moves.Learnset
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.weightedSelection
/**
* A function that selects a move from a given [Learnset] for a particular [FormData] at a certain level,
* taking into account any moves that have already been chosen for the moveset.
*
* @author Hiroku
* @since December 5th, 2025
*/
fun interface MoveSelector {
operator fun invoke(form: FormData, learnset: Learnset, level: Int, chosenMoves: Set<MoveTemplate>): MoveTemplate?
companion object {
val NONE = MoveSelector { _, _, _, _ -> null }
val LAST_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it] ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory != DamageCategories.STATUS } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_SUITABLE_OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
val suitableCategories = if (form.baseStats[Stats.ATTACK]!! > form.baseStats[Stats.SPECIAL_ATTACK]!!) {
setOf(DamageCategories.PHYSICAL)
} else if (form.baseStats[Stats.SPECIAL_ATTACK]!! > form.baseStats[Stats.ATTACK]!!) {
setOf(DamageCategories.SPECIAL)
} else {
setOf(DamageCategories.PHYSICAL, DamageCategories.SPECIAL)
}
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory in suitableCategories } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val LAST_STATUS = MoveSelector { form, learnset, level, chosenMoves ->
val levels = learnset.levelUpMoves.keys.filter { it <= level }.sortedDescending()
levels.forEach {
val moves = (learnset.levelUpMoves[it]?.filter { it.damageCategory == DamageCategories.STATUS } ?: return@forEach) - chosenMoves
if (moves.isNotEmpty()) {
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
}
return@MoveSelector null
}
val STAB = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory != DamageCategories.STATUS }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.PHYSICAL }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val stabMoves = moves.filter { it.elementalType in form.types && it.damageCategory == DamageCategories.SPECIAL }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val PHYSICAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val physicalMoves = moves.filter { it.damageCategory == DamageCategories.PHYSICAL }
return@MoveSelector physicalMoves.weightedSelection { it.getSelectionWeight(form) }
}
val SPECIAL = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val specialMoves = moves.filter { it.damageCategory == DamageCategories.SPECIAL }
return@MoveSelector specialMoves.weightedSelection { it.getSelectionWeight(form) }
}
val OFFENSIVE = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val offensiveMoves = moves.filter { it.damageCategory != DamageCategories.STATUS }
return@MoveSelector offensiveMoves.weightedSelection { it.getSelectionWeight(form) }
}
val RANDOM_LEVELUP = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val STATUS = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.getLevelUpMovesUpTo(level) - chosenMoves
val statusMoves = moves.filter { it.damageCategory == DamageCategories.STATUS }
return@MoveSelector statusMoves.weightedSelection { it.getSelectionWeight(form) }
}
val TM = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val STAB_TM = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.tmMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
val stabMoves = moves.filter { it.elementalType in form.types }
return@MoveSelector stabMoves.weightedSelection { it.getSelectionWeight(form) }
}
val EGG = MoveSelector { form, learnset, level, chosenMoves ->
val moves = learnset.eggMoves - chosenMoves - learnset.getLevelUpMovesUpTo(level)
return@MoveSelector moves.weightedSelection { it.getSelectionWeight(form) }
}
val selectors = mutableMapOf(
"none" to NONE,
"last_levelup" to LAST_LEVELUP,
"last_offensive" to LAST_OFFENSIVE,
"last_suitable_offensive" to LAST_SUITABLE_OFFENSIVE,
"last_status" to LAST_STATUS,
"levelup" to RANDOM_LEVELUP,
"stab" to STAB,
"stab_physical" to STAB_PHYSICAL,
"stab_special" to STAB_SPECIAL,
"physical" to PHYSICAL,
"special" to SPECIAL,
"offensive" to OFFENSIVE,
"status" to STATUS,
"tm" to TM,
"stab_tm" to STAB_TM,
"egg" to EGG
)
}
}
@@ -12,12 +12,12 @@ import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.moves.categories.DamageCategory
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.codec.CodecUtils
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.lang
@@ -53,8 +53,7 @@ open class MoveTemplate(
val pp: Int,
val priority: Int,
val critRatio: Double,
val effectChances: Array<Double>,
val weight: Float
val effectChances: Array<Double>
) {
val struct: MoStruct by lazy {
QueryStruct(hashMapOf())
@@ -70,9 +69,9 @@ open class MoveTemplate(
.addFunction("max_pp") { DoubleValue(maxPp) }
.addFunction("priority") { DoubleValue(priority) }
.addFunction("crit_ratio") { DoubleValue(critRatio) }
.addFunction("weight") { DoubleValue(weight.toDouble()) }
}
val displayName: MutableComponent
get() = lang("move.$name")
val description: MutableComponent
@@ -90,8 +89,7 @@ open class MoveTemplate(
pp = 5,
priority = 0,
critRatio = 0.0,
effectChances = emptyArray(),
weight = 0F,
effectChances = emptyArray()
)
companion object {
@@ -162,12 +160,4 @@ open class MoveTemplate(
}
return this.elementalType
}
fun getSelectionWeight(form: FormData): Float {
return if (this in form.signatureMoves) {
weight * MovesetBuilder.signatureMoveWeightMultiplier
} else {
weight
}
}
}
@@ -10,15 +10,15 @@ package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.data.MovesRegistrySyncPacket
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.fromJson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import net.minecraft.resources.ResourceLocation
@@ -46,14 +46,6 @@ object Moves : DataRegistry {
this.idMapping.clear()
this.moveScripts.clear()
val weights = mutableMapOf<String, Number>()
manager.getResourceStack(cobblemonResource("move_weights.json")).forEach {
it.open().use {
val map = GsonBuilder().create().fromJson<Map<String, Number>>(it.reader())
weights.putAll(map)
}
}
ShowdownService.service.resetRegistryData("move")
manager.listResources("moves") { it.path.endsWith(".js") }.forEach { (identifier, resource) ->
resource.open().use { stream ->
@@ -83,7 +75,6 @@ object Moves : DataRegistry {
val priority = jsMove.get("priority").asInt
val critRatio = jsMove.get("critRatio")?.asDouble ?: 1.0
val effectChances = arrayListOf<Double>()
val weight = weights[id]?.toFloat() ?: 50F
val secondariesMember = jsMove.get("secondaries")
val secondaryMember = jsMove.get("secondary")
if (secondariesMember != null && secondariesMember is JsonArray) {
@@ -101,20 +92,7 @@ object Moves : DataRegistry {
effectChances += secondaryMember.get("chance").asDouble
}
}
val move = MoveTemplate(
name = id,
num = num,
elementalType = elementalType,
damageCategory = damageCategory,
power = power,
target = target,
accuracy = accuracy,
pp = pp,
priority = priority,
critRatio = critRatio,
effectChances = effectChances.toTypedArray(),
weight = weight
)
val move = MoveTemplate(id, num, elementalType, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
this.register(move)
} catch (e: Exception) {
Cobblemon.LOGGER.error("Caught exception trying to resolve the move '{}'", id, e)
@@ -135,7 +113,7 @@ object Moves : DataRegistry {
@JvmStatic
fun getByNameOrDummy(name: String) = allMoves[name.lowercase()] ?: MoveTemplate.dummy(name.lowercase())
@JvmStatic
fun getExceptional() = getByName("tackle") ?: allMoves.values.first()
fun getExceptional() = getByName("tackle") ?: allMoves.values.random()
@JvmStatic
fun count() = allMoves.size
@JvmStatic
@@ -151,4 +129,5 @@ object Moves : DataRegistry {
this.allMoves[move.name] = move
this.idMapping[move.num] = move
}
}
@@ -1,38 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
/**
* Builders a moveset for a particular [FormData] and level. Probably only really need [DefaultMovesetBuilder] though.
*
* @author Hiroku
* @since December 5th, 2025
*/
interface MovesetBuilder {
/** The ID of this builder. It's autofilled from the registry loader. */
var id: ResourceLocation
/** The subtype designation for this moveset builder. Generally unused once read from JSON. */
val type: ResourceLocation
/** Builds a brand new moveset for the [form] and [level]. */
fun build(form: FormData, level: Int): MoveSet
companion object {
/** Weight multiplier for moves that are in the form's signatureMoves list. Makes them way more likely to be chosen. */
var signatureMoveWeightMultiplier = 5F
/** Subtypes of MovesetBuilder. I personally doubt you'll need any other subtypes, but hey. */
val movesetBuilderTypes = mutableMapOf<ResourceLocation, Class<out MovesetBuilder>>(
cobblemonResource("default") to DefaultMovesetBuilder::class.java
)
}
}
@@ -8,14 +8,12 @@
package com.cobblemon.mod.common.api.npc
import com.cobblemon.mod.common.api.npc.partyproviders.ComposedPoolPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.PoolPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.ScriptPartyProvider
import com.cobblemon.mod.common.api.npc.partyproviders.SimplePartyProvider
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.google.gson.JsonElement
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
/**
@@ -27,15 +25,14 @@ import net.minecraft.server.level.ServerPlayer
*/
interface NPCPartyProvider {
companion object {
val types = mutableMapOf(
SimplePartyProvider.TYPE to ::SimplePartyProvider,
PoolPartyProvider.TYPE to ::PoolPartyProvider,
ScriptPartyProvider.TYPE to ::ScriptPartyProvider,
ComposedPoolPartyProvider.TYPE to ::ComposedPoolPartyProvider
val types = mutableMapOf<String, (String) -> NPCPartyProvider>(
SimplePartyProvider.TYPE to { SimplePartyProvider() },
PoolPartyProvider.TYPE to { PoolPartyProvider() },
ScriptPartyProvider.TYPE to { ScriptPartyProvider() }
)
}
val type: ResourceLocation
val type: String
val isStatic: Boolean
fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer> = emptyList()): NPCPartyStore
// Why did I opt for manual JSON loading??? I must have had a reason but I can't remember. Maybe for S2C? Use a codec doofus
@@ -1,68 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import kotlin.random.Random
/**
* A kind of definition for how to compose a party of Pokémon given a [PartyPool].
*
* @author Hiroku
* @since December 7th, 2025
*/
class PartyComposition {
var id = cobblemonResource("dummy_composition")
// val displayName = "cobblemon.party_composition.$id" // dunno if i will use this yet
val scrambleOrder: Boolean = false
val slot1 = listOf<String>()
val slot2 = listOf<String>()
val slot3 = listOf<String>()
val slot4 = listOf<String>()
val slot5 = listOf<String>()
val slot6 = listOf<String>()
fun compose(pool: PartyPool, level: Int, aspects: Set<String>, desiredPokemonCount: Int, runtime: MoLangRuntime, random: Random = Random.Default): List<Pokemon> {
val chosenEntries = mutableListOf<PartyPool.PoolEntry>()
val availableEntries = pool.entries.filter { level in it.npcLevels.resolve(runtime) && (it.npcAspects.isEmpty() || it.npcAspects.all(aspects::contains)) }
for (labelSet in listOf(slot1, slot2, slot3, slot4, slot5, slot6)) {
pool.tryChoosingEntry(labelSet, runtime, random, availableEntries, chosenEntries)
if (chosenEntries.size >= desiredPokemonCount) {
break
}
}
val finalEntries = if (scrambleOrder) {
chosenEntries.shuffled(random)
} else {
chosenEntries
}
val pokemon = finalEntries.map { entry ->
val levelVariation = entry.levelVariation.resolve(runtime)
val lvlVariation = if (levelVariation.first == levelVariation.last) {
levelVariation.first
} else {
levelVariation.random(random)
}
val finalLevel = level + lvlVariation
val properties = entry.pokemon.copy()
properties.level = properties.level ?: finalLevel
val pokemon = properties.create()
entry.movesetBuilders.randomOrNull()?.let(pokemon::initializeMovesetFrom)
pokemon
}
return pokemon
}
}
@@ -1,77 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.toProperties
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
/**
* A pool of Pokémon to choose from, typically for generating a party for an NPC. The entries
* of the pool work on a weight and label system which is used as part of a [PartyComposition].
*
* @author Hiroku
* @since December 7th, 2025
*/
class PartyPool {
var id = cobblemonResource("dummy")
val displayName = "cobblemon.party_pool.$id"
val entries = mutableListOf<PoolEntry>()
class PoolEntry {
val pokemon: PokemonProperties = "pikachu".toProperties()
val labels: List<String> = emptyList()
val npcAspects: List<String> = emptyList()
val required: List<String> = emptyList()
val excluded: List<String> = emptyList()
val maxSelectableTimes: ExpressionLike = "6".asExpressionLike()
val levelVariation: ScriptableIntRange = ScriptableIntRange(0, 0)
val npcLevels: ScriptableIntRange = ScriptableIntRange(1, 100)
val movesetBuilders: List<MovesetBuilder> = emptyList()
val weight: ExpressionLike = "50".asExpressionLike()
}
/**
* Goes through each of the labels in order, trying to find a valid choice. Once it finds one, it skips the later
* labels and adds the value to [chosenEntries] and returns. If none of the labels could find a valid entry, nothing is added.
*/
fun tryChoosingEntry(
labels: List<String>,
runtime: MoLangRuntime,
random: Random,
availableEntries: List<PoolEntry>,
chosenEntries: MutableList<PoolEntry>,
) {
for (label in labels) {
val entries = availableEntries
.filter { label in it.labels || label.lowercase() == "any" }
.filter { entry ->
// Check required labels
entry.required.all { it in labels } &&
// Check excluded labels
entry.excluded.none { it in labels } &&
// Check max selectable times
chosenEntries.count { it == entry } < entry.maxSelectableTimes.resolveFloat(runtime)
}
if (entries.isNotEmpty()) {
val selectedEntry = entries.weightedSelection(random) { it.weight.resolveFloat(runtime) } ?: return
chosenEntries.add(selectedEntry)
return
}
}
}
}
@@ -1,94 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.npc.partyproviders
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.CobblemonPartyCompositions
import com.cobblemon.mod.common.CobblemonPartyPools
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.api.npc.PartyComposition
import com.cobblemon.mod.common.api.npc.PartyPool
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.withQueryValue
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import kotlin.random.Random
import kotlin.ranges.random
import net.minecraft.server.level.ServerPlayer
/**
* An [NPCPartyProvider] which uses a [PartyComposition] and [PartyPool] to generate a party.
*
* There are controls for minimum and maximum Pokémon count, as well as whether to use a fixed random seed for the NPC.
*
* @author Hiroku
* @since December 8th, 2025
*/
class ComposedPoolPartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("composed_pool")
}
override val type = TYPE
override var isStatic: Boolean = true
var useFixedRandom: Boolean = false
var minPokemon: Expression = "1".asExpression()
var maxPokemon: Expression = "6".asExpression()
lateinit var pool: PartyPool
lateinit var composition: PartyComposition
override fun loadFromJSON(json: JsonElement) {
json as JsonObject
isStatic = json.getAsJsonPrimitive("isStatic")?.asBoolean ?: true
useFixedRandom = json.getAsJsonPrimitive("useFixedRandom")?.asBoolean ?: false
pool = CobblemonPartyPools.partyPools[json.get("pool").asString.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown PartyPool id: ${json.get("pool").asString}")
composition = CobblemonPartyCompositions.partyCompositions[json.get("composition").asString.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown PartyComposition id: ${json.get("composition").asString}")
minPokemon = json.getAsJsonPrimitive("minPokemon").asString?.asExpression() ?: "1".asExpression()
maxPokemon = json.getAsJsonPrimitive("maxPokemon").asString?.asExpression() ?: "6".asExpression()
}
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
val runtime = MoLangRuntime().setup().withQueryValue("npc", npc.struct)
val random = if (useFixedRandom) Random(npc.uuid.hashCode()) else Random.Default
runtime.withQueryValue("level", DoubleValue(level))
runtime.withQueryValue("players", players.asArrayValue { it.asMoLangValue() })
if (players.size == 1) {
// This is for the convenience, most cases will be pvn one player
runtime.withQueryValue("player", players.first().asMoLangValue())
}
val minPokemon = runtime.resolveInt(this.minPokemon)
val maxPokemon = runtime.resolveInt(this.maxPokemon)
val desiredPokemonCount = (minPokemon..maxPokemon).random(random)
val pokemonList = composition.compose(
pool = pool,
level = level,
aspects = npc.aspects,
desiredPokemonCount = desiredPokemonCount,
runtime = runtime,
random = random
)
val partyStore = NPCPartyStore(npc)
pokemonList.forEach(partyStore::add)
return partyStore
}
}
@@ -48,7 +48,7 @@ import kotlin.random.Random
*/
class PoolPartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("pool")
const val TYPE = "pool"
}
@Transient
@@ -150,10 +150,10 @@ class PoolPartyProvider : NPCPartyProvider {
val instance = selected.pokemon.copy().also { it.level = it.level ?: dictatedLevel ?: randomLevel }.create()
party.add(instance)
}
if (party.none()) {
LOGGER.error("$npc had no Pokémon produced by their party provider")
LOGGER.error("${npc} has no Pokemon on Party")
}
}
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
@@ -28,7 +28,7 @@ import net.minecraft.server.level.ServerPlayer
class ScriptPartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("script")
const val TYPE = "script"
}
override val type = TYPE
@@ -16,7 +16,6 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getBooleanOrNull
import com.cobblemon.mod.common.util.toProperties
import com.google.gson.JsonElement
@@ -30,7 +29,7 @@ import net.minecraft.server.level.ServerPlayer
*/
class SimplePartyProvider : NPCPartyProvider {
companion object {
val TYPE = cobblemonResource("simple")
const val TYPE = "simple"
}
@Transient
@@ -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", throwPower = 0.75f, ancient = true)
createDefault("ancient_gigaton_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_feather_ball", 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_wing_ball", MultiplierModifier(1.5F), throwPower = 2.5f, ancient = true)
createDefault("ancient_jet_ball", MultiplierModifier(2F), 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 ->
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.pokemon
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.abilities.Ability
import com.cobblemon.mod.common.api.events.CobblemonEvents
@@ -149,13 +148,6 @@ open class PokemonProperties {
props.originalTrainerType = OriginalTrainerType.entries.toList().parsePropertyOfCollection(keyPairs, listOf("originaltrainertype", "ottype"), labelsOptional = true) { it.name.lowercase() }
props.originalTrainer = parsePlayerProperty(keyPairs, listOf("originaltrainer", "ot"))
props.moves = parseString(keyPairs, listOf("moves"))?.split(",")
props.movesetBuilders = parseString(keyPairs, listOf("moveset_builders", "movesetbuilders", "moveset_builder", "movesetbuilder"))?.split(",")?.mapNotNull {
try {
it.asIdentifierDefaultingNamespace()
} catch (_: ResourceLocationException) {
null
}
}
props.heldItem = parseString(keyPairs, listOf("helditem", "held_item"))
props.minPerfectIVs = parseIntProperty(keyPairs, listOf("min_perfect_ivs"))?.coerceIn(0, Stats.PERMANENT.size)
props.scaleModifier = parseFloatProperty(keyPairs, listOf("scale_modifier"))
@@ -364,7 +356,6 @@ open class PokemonProperties {
var originalTrainerType: OriginalTrainerType? = null
var originalTrainer: String? = null // Original Trainer by Username or UUID
var moves: List<String>? = null
var movesetBuilders: List<ResourceLocation>? = null
var heldItem: String? = null
var cosmeticItem: String? = null
var scaleModifier: Float? = null
@@ -471,7 +462,7 @@ open class PokemonProperties {
}
moves?.let { moves ->
if (pokemon.moveSet.getMoves().isEmpty()) {
pokemon.initializeMovesetFromDefault()
pokemon.initializeMoveset(preferLatest = true)
}
val moveTemplates = moves.mapNotNull(Moves::getByName).shuffled()
val replaceableIndices = (0..3).filterNot { pokemon.moveSet[it]?.template?.let(moveTemplates::contains) == true }.shuffled()
@@ -484,13 +475,6 @@ open class PokemonProperties {
pokemon.moveSet.update()
}
}
movesetBuilders?.let { movesetBuilders ->
val builders = movesetBuilders.mapNotNull { CobblemonMovesetBuilders.movesetBuilders[it] }
if (builders.isNotEmpty()) {
val movesetBuilder = builders.random()
pokemon.initializeMovesetFrom(movesetBuilder)
}
}
heldItem?.let { itemKey ->
val server = server() ?: return@let
val parser = ItemParser(server.registryAccess())
@@ -694,7 +678,6 @@ open class PokemonProperties {
originalTrainerType?.let { nbt.putInt(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.ordinal) }
originalTrainer?.let { nbt.putString(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
moves?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
movesetBuilders?.let { nbt.putString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
heldItem?.let {nbt.putString(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
scaleModifier?.let { nbt.putFloat(DataKeys.POKEMON_SCALE_MODIFIER, it) }
val custom = ListTag()
@@ -729,7 +712,6 @@ open class PokemonProperties {
originalTrainerType = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) OriginalTrainerType.valueOf(tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)) else null
originalTrainer = if (tag.contains(DataKeys.POKEMON_ORIGINAL_TRAINER)) tag.getString(DataKeys.POKEMON_ORIGINAL_TRAINER) else null
moves = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVES)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVES).split(",") else null
movesetBuilders = if (tag.contains(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)) tag.getString(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS).split(",").map { it.asIdentifierDefaultingNamespace() } else null
heldItem = if (tag.contains(DataKeys.POKEMON_PROPERTIES_HELDITEM)) tag.getString(DataKeys.POKEMON_PROPERTIES_HELDITEM) else null
scaleModifier = if (tag.contains(DataKeys.POKEMON_SCALE_MODIFIER)) tag.getFloat(DataKeys.POKEMON_SCALE_MODIFIER) else null
val custom = tag.getList(DataKeys.POKEMON_PROPERTIES_CUSTOM, Tag.TAG_STRING.toInt())
@@ -765,7 +747,6 @@ open class PokemonProperties {
originalTrainerType?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE, it.name) }
originalTrainer?.let { json.addProperty(DataKeys.POKEMON_ORIGINAL_TRAINER, it) }
moves?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVES, it.joinToString(separator = ",")) }
movesetBuilders?.let { json.addProperty(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS, it.joinToString(separator = ",") { source -> source.toString() }) }
heldItem?.let {json.addProperty(DataKeys.POKEMON_PROPERTIES_HELDITEM, it)}
scaleModifier?.let { json.addProperty(DataKeys.POKEMON_SCALE_MODIFIER, it) }
val custom = JsonArray()
@@ -800,7 +781,6 @@ open class PokemonProperties {
originalTrainerType = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER_TYPE)?.asString?.let { OriginalTrainerType.valueOf(it) }
originalTrainer = json.get(DataKeys.POKEMON_ORIGINAL_TRAINER)?.asString
moves = json.get(DataKeys.POKEMON_PROPERTIES_MOVES)?.asString?.split(",")
movesetBuilders = json.get(DataKeys.POKEMON_PROPERTIES_MOVESET_BUILDERS)?.asString?.split(",")?.map { it.asIdentifierDefaultingNamespace() }
heldItem = json.get(DataKeys.POKEMON_PROPERTIES_HELDITEM)?.asString
scaleModifier = json.get(DataKeys.POKEMON_SCALE_MODIFIER)?.asFloat
val custom = json.get(DataKeys.POKEMON_PROPERTIES_CUSTOM)?.asJsonArray
@@ -840,7 +820,6 @@ open class PokemonProperties {
originalTrainer?.let { pieces.add("originaltrainer=$it") }
customProperties.forEach { pieces.add(it.asString()) }
moves?.let { pieces.add("moves=${it.joinToString(separator = ",")}") }
movesetBuilders?.let { pieces.add("movesetbuilders=${it.joinToString(separator = ",") { source -> source.toString() }}") }
heldItem?.let {pieces.add("helditem=$it")}
scaleModifier?.let { pieces.add("scale_modifier=$it") }
return pieces.joinToString(separator)
@@ -23,7 +23,6 @@ import com.cobblemon.mod.common.api.drop.ItemDropMethod
import com.cobblemon.mod.common.api.entity.EntityDimensionsAdapter
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.moves.adapters.MoveTemplateAdapter
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies.getByIdentifier
@@ -132,7 +131,6 @@ object PokemonSpecies : JsonDataRegistry<Species> {
.registerTypeAdapter(RidingBehaviourSettings::class.java, RidingBehaviourSettingsAdapter)
.registerTypeAdapter(RideSoundSettingsList::class.java, RideSoundSettingsListAdapter)
.registerTypeAdapter(ObtainableItemCondition::class.java, ObtainableItemConditionAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
.disableHtmlEscaping()
.enableComplexMapKeySerialization()
.create()
@@ -316,11 +314,11 @@ object PokemonSpecies : JsonDataRegistry<Species> {
val result = mutableMapOf<String, String>()
this.species.forEach {species ->
val baseSpecies = ShowdownSpecies(species, null)
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name, "")] = this.gson.toJson(baseSpecies)
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name.lowercase(), "")] = this.gson.toJson(baseSpecies)
species.forms.forEach { form ->
if (form != species.standardForm) {
val formSpecies = ShowdownSpecies(species, form)
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name, "")] = this.gson.toJson(formSpecies)
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name.lowercase(), "")] = this.gson.toJson(formSpecies)
}
}
}
@@ -54,8 +54,6 @@ class RidingAnimationData(val ride: PokemonEntity)
fun update() {
val activeRide = ride.hasControllingPassenger()
if (!activeRide) return
var currRot = Vec3.ZERO
var angDelta = Vec3.ZERO
@@ -153,14 +151,16 @@ class RidingAnimationData(val ride: PokemonEntity)
/******************************************************
* Update springs
*****************************************************/
val driver = ride.controllingPassenger
val vertInput = when {
driver != null && driver.jumping-> 1.0
driver != null && driver.isShiftKeyDown -> -1.0
else -> 0.0
if (activeRide) {
val driver = ride.controllingPassenger
val vertInput = when {
driver != null && driver.jumping-> 1.0
driver != null && driver.isShiftKeyDown -> -1.0
else -> 0.0
}
val input = if(driver != null) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
driverInputSpring.update(input, stiffness * 2, damping)
}
val input = if(driver != null) Vec3(driver.xxa.toDouble() * -1, vertInput, driver.zza.toDouble()) else Vec3.ZERO
driverInputSpring.update(input, stiffness * 2, damping)
rotDeltaSpring.update(angDelta, stiffness, damping)
velocitySpring.update(currentVelocity, stiffness, damping)
rotSpring.update(currRot, stiffness, damping)
@@ -44,7 +44,7 @@ import kotlin.random.Random
* @since July 10th, 2022
*/
open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelectionData> {
class SelectingSpawnInformation(val spawnDetail: SpawnDetail) {
class SelectingSpawnInformation {
val spawnablePositions = mutableMapOf<SpawnablePosition, Float>()
var highestWeight = 0F
@@ -58,81 +58,61 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
fun chooseSpawnablePosition() = spawnablePositions.entries.weightedSelection { it.value }!!.key
}
class SpawnablePositionBucketSelectionData(
val selectingSpawnInformation: MutableList<SelectingSpawnInformation>
) {
var percentSum: Float = 0F
val seenSpawns = mutableListOf<SpawnDetail>()
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
// the percentage will get amplified for every spawnable position the thing was possible, completely
// ruining the point of this pre-selection percentage.
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
seenSpawns.add(spawnDetail)
percentSum += spawnDetail.percentage
}
val spawnInformation = selectingSpawnInformation.find { it.spawnDetail == spawnDetail }
?: SelectingSpawnInformation(spawnDetail).also { selectingSpawnInformation.add(it) }
spawnInformation.add(spawnablePosition, spawnablePosition.getWeight(spawnDetail))
}
fun remove(spawnInformation: SelectingSpawnInformation) {
selectingSpawnInformation -= spawnInformation
if (spawnInformation.spawnDetail.percentage > 0) {
percentSum -= spawnInformation.spawnDetail.percentage
}
}
}
class SpawnablePositionSelectionData(
val spawner: Spawner,
val spawnablePositions: List<SpawnablePosition>,
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData>,
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
var percentSum: Float
): SpawnSelectionData {
val size: Int
get() = spawnToSpawnablePosition.size
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
fun getDataForBucket(bucket: SpawnBucket): SpawnablePositionBucketSelectionData {
val existing = selectingSpawnInformation[bucket]
if (existing != null) {
return existing
}
val bucketSpawnInformation = SpawnablePositionBucketSelectionData(mutableListOf())
spawnablePositions.forEach { spawnablePosition ->
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
bucketSpawnInformation.add(it, spawnablePosition)
}
}
selectingSpawnInformation[bucket] = bucketSpawnInformation
return bucketSpawnInformation
}
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation.filter { shouldRemove(it.spawnDetail) } }
.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
}
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
val toRemove = selectingSpawnInformation.flatMap { it.value.selectingSpawnInformation }.filter { positionData ->
positionData.spawnablePositions.removeIf { shouldRemove(positionData.spawnDetail, it.key) }
val toRemove = spawnToSpawnablePosition.entries.filter { (spawnDetail, positionData) ->
positionData.spawnablePositions.removeIf { shouldRemove(spawnDetail, it.key) }
positionData.highestWeight = positionData.spawnablePositions.maxOfOrNull { it.value } ?: 0F
positionData.spawnablePositions.isEmpty()
}
toRemove.forEach { selectingSpawnInformation[it.spawnDetail.bucket]?.remove(it) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
percentSum -= toRemove.sumOf { it.key.percentage.toDouble().takeIf { it > 0 } ?: 0.0 }.toFloat()
}
}
override fun getSelectionData(
spawner: Spawner,
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): SpawnablePositionSelectionData {
val selectingSpawnInformation: MutableMap<SpawnBucket, SpawnablePositionBucketSelectionData> = mutableMapOf()
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation> = mutableMapOf()
var percentSum = 0F
return SpawnablePositionSelectionData(spawner, spawnablePositions, selectingSpawnInformation)
spawnablePositions.forEach { spawnablePosition ->
spawner.getMatchingSpawns(bucket, spawnablePosition).forEach {
// Only add to percentSum if this is the first time we've seen this SpawnDetail, otherwise
// the percentage will get amplified for every spawnable position the thing was possible, completely
// ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !spawnToSpawnablePosition.containsKey(it)) {
percentSum += it.percentage
}
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(
it,
::SelectingSpawnInformation
)
selectingSpawnInformation.add(spawnablePosition, spawnablePosition.getWeight(it))
}
}
return SpawnablePositionSelectionData(spawnToSpawnablePosition, percentSum)
}
override fun selectSpawnAction(
@@ -140,8 +120,8 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
bucket: SpawnBucket,
selectionData: SpawnablePositionSelectionData,
): SpawnAction<*>? {
val selectingSpawnInformation = selectionData.getDataForBucket(bucket) ?: return null
var percentSum = selectingSpawnInformation.percentSum
val spawnToSpawnablePosition = selectionData.spawnToSpawnablePosition
var percentSum = selectionData.percentSum
// First pass is doing percentage checks.
if (percentSum > 0) {
@@ -149,7 +129,7 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
if (percentSum > 100) {
Cobblemon.LOGGER.warn(
"""
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
This means you don't understand how this option works.
""".trimIndent()
)
@@ -160,13 +140,13 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
* It's [0, 1) and I want (0, 1]
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
*/
val selectedPercentage = 100 - Random.nextFloat() * 100
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
percentSum = 0F
for (info in selectingSpawnInformation.selectingSpawnInformation) {
if (info.spawnDetail.percentage > 0) {
percentSum += info.spawnDetail.percentage
for ((spawnDetail, info) in spawnToSpawnablePosition) {
if (spawnDetail.percentage > 0) {
percentSum += spawnDetail.percentage
if (percentSum >= selectedPercentage) {
return info.spawnDetail.choose(
return spawnDetail.choose(
spawnablePosition = info.chooseSpawnablePosition(),
bucket = bucket,
selectionData = selectionData
@@ -176,9 +156,9 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
}
}
val selectedSpawn = selectingSpawnInformation.selectingSpawnInformation.toList().weightedSelection { it.highestWeight }!!
return selectedSpawn.spawnDetail.choose(
spawnablePosition = selectedSpawn.chooseSpawnablePosition(),
val selectedSpawn = spawnToSpawnablePosition.entries.toList().weightedSelection { it.value.highestWeight }!!
return selectedSpawn.key.choose(
spawnablePosition = selectedSpawn.value.chooseSpawnablePosition(),
bucket = bucket,
selectionData = selectionData
)
@@ -189,16 +169,16 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): Map<SpawnDetail, Float> {
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
if (selectionData.selectingSpawnInformation.isEmpty()) {
if (selectionData.size == 0) {
return emptyMap()
}
val totalWeights = mutableMapOf<SpawnDetail, Float>()
for (info in selectionData.selectingSpawnInformation) {
totalWeights[info.spawnDetail] = info.highestWeight
for ((spawnDetail, info) in selectionData.spawnToSpawnablePosition.entries) {
totalWeights[spawnDetail] = info.highestWeight
}
return totalWeights
@@ -18,6 +18,7 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.util.removeIf
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
import kotlin.reflect.full.createInstance
/**
* A spawning selector that compiles a distinct list of all spawn details that
@@ -32,9 +33,9 @@ import kotlin.random.Random
* of an entire spawnable position type to favour whichever are the more populous.
*
* The weight for a spawn when doing primary selection is whichever spawnable position-influenced
* weight is highest (as weight multipliers exist per spawnable position). After the spawn has been
* chosen, the selection of which spawnable position to spawn it at is selected via another weighted
* selection using spawnable position-adjusted weights.
* weight is highest (as weight multipliers exist per spawnable position) and then the
* selection of which spawnable position to spawn the primary selected spawn uses the spawnable
* position-adjusted weight.
*
* At a glance:
* - Spawn detail selection is flat across spawnable position quantity
@@ -44,6 +45,7 @@ import kotlin.random.Random
* @since July 10th, 2022
*/
open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnablePositionWeightedSelector.SeparatedSelectionData> {
open fun getWeight(spawnablePositionType: SpawnablePositionType<*>) = spawnablePositionType.getWeight()
class SelectingSpawnInformation {
val spawnablePositionWeights = mutableMapOf<SpawnablePosition, Float>()
@@ -60,25 +62,13 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
fun chooseSpawnablePosition() = spawnablePositionWeights.keys.toList().weightedSelection { spawnablePositionWeights[it]!! }!!
}
class SpawnablePositionSelectionData {
val spawnToSpawnablePosition = mutableMapOf<SpawnDetail, SelectingSpawnInformation>()
var percentSum: Float = 0F
val seenSpawns = mutableListOf<SpawnDetail>()
class SpawnablePositionSelectionData(
val spawnToSpawnablePosition: MutableMap<SpawnDetail, SelectingSpawnInformation>,
var percentSum: Float
) {
val size: Int
get() = spawnToSpawnablePosition.size
fun add(spawnDetail: SpawnDetail, spawnablePosition: SpawnablePosition) {
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
// position type, otherwise the percentage will get amplified for every spawnable position the thing
// was possible, completely ruining the point of this pre-selection percentage.
if (spawnDetail.percentage > 0 && spawnDetail !in seenSpawns) {
percentSum += spawnDetail.percentage
seenSpawns.add(spawnDetail)
}
val selectingSpawnInformation = spawnToSpawnablePosition.getOrPut(spawnDetail, ::SelectingSpawnInformation)
selectingSpawnInformation.add(spawnDetail, spawnablePosition, 1F)
}
fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
val toRemove = spawnToSpawnablePosition.entries.filter { shouldRemove(it.key) }
toRemove.forEach { spawnToSpawnablePosition.remove(it.key) }
@@ -99,68 +89,67 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
}
class SeparatedSelectionData(
val spawner: Spawner,
val spawnablePositions: List<SpawnablePosition>
) : SpawnSelectionData {
val spawnablePositionTypeToSpawns: MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>
): SpawnSelectionData {
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
val bucketData = mutableMapOf<SpawnBucket, MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData>>()
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {
bucketData.values.forEach {
it.values.forEach { it.removeSpawnDetails(shouldRemove) }
it.removeIf { it.value.size == 0 }
}
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnDetails(shouldRemove) }
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
}
override fun removeSpawnablePositions(shouldRemove: (SpawnDetail, SpawnablePosition) -> Boolean) {
bucketData.values.forEach {
it.values.forEach { it.removeSpawnablePositions(shouldRemove) }
it.removeIf { it.value.size == 0 }
}
}
fun getDataForBucket(bucket: SpawnBucket): MutableMap<SpawnablePositionType<*>, SpawnablePositionSelectionData> {
bucketData[bucket]?.let { return it }
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
spawnablePositions.forEach { spawnablePosition ->
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
if (possible.isNotEmpty()) {
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns
.getOrPut(spawnablePositionType, ::SpawnablePositionSelectionData)
possible.forEach { spawnablePositionSelectionData.add(it, spawnablePosition) }
}
}
bucketData[bucket] = spawnablePositionTypeToSpawns
return spawnablePositionTypeToSpawns
spawnablePositionTypeToSpawns.values.forEach { it.removeSpawnablePositions(shouldRemove) }
spawnablePositionTypeToSpawns.removeIf { it.value.size == 0 }
}
}
override fun getSelectionData(
spawner: Spawner,
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
) = SeparatedSelectionData(spawner, spawnablePositions)
): SeparatedSelectionData {
val spawnablePositionTypeToSpawns = mutableMapOf<SpawnablePositionType<*>, SpawnablePositionSelectionData>()
spawnablePositions.forEach { spawnablePosition ->
val spawnablePositionType = SpawnablePosition.getByClass(spawnablePosition)!!
val possible = spawner.getMatchingSpawns(bucket, spawnablePosition)
if (possible.isNotEmpty()) {
val spawnablePositionSelectionData = spawnablePositionTypeToSpawns.getOrPut(spawnablePositionType) { SpawnablePositionSelectionData(mutableMapOf(), 0F) }
possible.forEach {
// Only add to percentSum if this is the first time we've seen this SpawnDetail for this spawnable
// position type, otherwise the percentage will get amplified for every spawnable position the thing
// was possible, completely ruining the point of this pre-selection percentage.
if (it.percentage > 0 && !spawnablePositionSelectionData.spawnToSpawnablePosition.containsKey(it)) {
spawnablePositionSelectionData.percentSum += it.percentage
}
val selectingSpawnInformation = spawnablePositionSelectionData.spawnToSpawnablePosition.getOrPut(
it,
SelectingSpawnInformation::class::createInstance
)
selectingSpawnInformation.add(it, spawnablePosition, getWeight(spawnablePositionType))
}
}
}
return SeparatedSelectionData(spawnablePositionTypeToSpawns)
}
override fun selectSpawnAction(
spawner: Spawner,
bucket: SpawnBucket,
selectionData: SeparatedSelectionData
): SpawnAction<*>? {
val bucketData = selectionData.getDataForBucket(bucket)
if (bucketData.isEmpty()) {
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
return null
}
// Which spawnable position type should we use?
val spawnablePositionSelectionData = bucketData.entries.toList()
.weightedSelection { it.key.getWeight() * it.value.size }
val spawnablePositionSelectionData = selectionData.spawnablePositionTypeToSpawns.entries.toList()
.weightedSelection { getWeight(it.key) * it.value.size }
?.value
?: return null
@@ -172,7 +161,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
if (percentSum > 100) {
LOGGER.warn(
"""
A spawn list for ${spawner.name} exceeded 100% on percentage sums in bucket ${bucket.name}...
A spawn list for ${spawner.name} exceeded 100% on percentage sums...
This means you don't understand how this option works.
""".trimIndent()
)
@@ -184,7 +173,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
* It's [0, 1) and I want (0, 1]
* See half-open intervals here https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology
*/
val selectedPercentage = 100 - Random.nextFloat() * 100
val selectedPercentage = 100 - Random.Default.nextFloat() * 100
percentSum = 0F
for ((spawnDetail, info) in spawnToSpawnablePosition) {
if (spawnDetail.percentage > 0) {
@@ -235,8 +224,7 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
val percentageWeight = (rescaledTotalWeight - totalWeight) / percentSum
for ((spawnDetail, info) in spawnToSpawnablePosition.entries) {
totalWeights[spawnDetail] =
info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
totalWeights[spawnDetail] = info.highestWeight + if (spawnDetail.percentage > 0) spawnDetail.percentage * percentageWeight else 0F
}
return totalWeights
@@ -247,18 +235,18 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
bucket: SpawnBucket,
spawnablePositions: List<SpawnablePosition>
): Map<SpawnDetail, Float> {
val selectionData = getSelectionData(spawner, spawnablePositions).getDataForBucket(bucket)
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
if (selectionData.isEmpty()) {
if (selectionData.spawnablePositionTypeToSpawns.isEmpty()) {
return mapOf()
}
val totalWeights = mutableMapOf<SpawnDetail, Float>()
val totalSpawnablePositionWeight = selectionData.keys.sumOf { it.getWeight().toDouble() }.toFloat()
val totalSpawnablePositionWeight = selectionData.spawnablePositionTypeToSpawns.keys.sumOf { getWeight(it).toDouble() }.toFloat()
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData) {
val spawnPositionWeightCorrection = spawnablePositionType.getWeight() / totalSpawnablePositionWeight
for ((spawnablePositionType, spawnPositionSelectionData) in selectionData.spawnablePositionTypeToSpawns) {
val spawnPositionWeightCorrection = getWeight(spawnablePositionType) / totalSpawnablePositionWeight
val spawnPositionProbabilities = getProbabilitiesFromSpawnablePositionType(spawner, spawnPositionSelectionData)
spawnPositionProbabilities.entries.forEach {
@@ -29,7 +29,7 @@ interface SpawningSelector<T : SpawnSelectionData> {
val DEFAULT = FlatSpawnablePositionWeightedSelector()
}
fun getSelectionData(spawner: Spawner, spawnablePositions: List<SpawnablePosition>): T
fun getSelectionData(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>): T
fun selectSpawnAction(
spawner: Spawner,
@@ -37,18 +37,12 @@ interface SpawningSelector<T : SpawnSelectionData> {
selectionData: T
): SpawnAction<*>?
fun select(
spawner: Spawner,
bucketSelector: () -> SpawnBucket,
spawnablePositions: List<SpawnablePosition>,
maxSpawns: Int
): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, spawnablePositions)
fun select(spawner: Spawner, bucket: SpawnBucket, spawnablePositions: List<SpawnablePosition>, maxSpawns: Int): List<SpawnAction<*>> {
val selectionData = getSelectionData(spawner, bucket, spawnablePositions)
val spawnActions = selectionData.spawnActions
while (spawnActions.size < maxSpawns) {
val bucket = bucketSelector()
val spawnAction = selectSpawnAction(
spawner = spawner,
bucket = bucket,
@@ -29,6 +29,13 @@ import com.cobblemon.mod.common.util.isBoxLoaded
import com.cobblemon.mod.common.util.squeezeWithinBounds
import com.cobblemon.mod.common.util.toVec3f
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.Any
import kotlin.Boolean
import kotlin.Float
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.collections.plus
import kotlin.math.max
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
@@ -88,9 +95,10 @@ interface Spawner {
): SpawnAction<*>? {
influences.removeIf { it.isExpired() }
spawnablePosition.influences.addAll(influences)
val bucket = chooseBucket(cause, spawnablePosition.influences)
return selector.select(
spawner = this,
bucketSelector = { chooseBucket(cause, spawnablePosition.influences) },
bucket = bucket,
spawnablePositions = listOf(spawnablePosition),
maxSpawns = 1
).firstOrNull()
@@ -108,9 +116,9 @@ interface Spawner {
val areaBox = AABB.ofSize(
Vec3(constrainedArea.getCenter().toVec3f()),
ENTITY_LIMIT_CHUNK_RANGE * 16.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2,
1000.0,
ENTITY_LIMIT_CHUNK_RANGE * 16.0
ENTITY_LIMIT_CHUNK_RANGE * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
@@ -132,10 +140,11 @@ interface Spawner {
val zone = generator.generate(this, constrainedArea)
val spawnablePositions = resolver.resolve(this, prioritizedAreaCalculators, zone)
val influences = influences + zone.unconditionalInfluences
val bucket = chooseBucket(zoneInput.cause, influences)
return selector.select(
spawner = this,
bucketSelector = { chooseBucket(zoneInput.cause, influences) },
bucket = bucket,
spawnablePositions = spawnablePositions,
maxSpawns = maxSpawns
)
@@ -94,7 +94,8 @@ enum class ShowdownActionResponseType(val loader: (RegistryFriendlyByteBuf) -> S
PASS({ PassActionResponse }),
SHIFT({ ShiftActionResponse()}),
HEAL_ITEM({ HealItemActionResponse("potion") }),
FORFEIT({ ForfeitActionResponse() });
FORFEIT({ ForfeitActionResponse() }),
FLEE_ATTEMPT({ FleeAttemptActionResponse() }),
}
abstract class ShowdownActionResponse(val type: ShowdownActionResponseType) {
@@ -280,6 +281,11 @@ class ForfeitActionResponse : ShowdownActionResponse(ShowdownActionResponseType.
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "forfeit"
}
class FleeAttemptActionResponse : ShowdownActionResponse(ShowdownActionResponseType.FLEE_ATTEMPT) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean) = true
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "flee-attempt"
}
class ShowdownMoveset {
lateinit var moves: List<InBattleMove>
var trapped = false
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.battles.runner.ShowdownService
import net.minecraft.CrashReport
import java.util.*
import java.util.concurrent.CountDownLatch
@@ -37,8 +38,15 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
override fun run() {
LOGGER.info("Starting showdown service...")
ShowdownService.service.openConnection()
LOGGER.info("Showdown has been started!")
this.latch.countDown()
try {
ShowdownService.service.openConnection()
LOGGER.info("Showdown has been started!")
} catch (e: Exception) {
LOGGER.error("Failed to start showdown service!", e)
} finally {
this.latch.countDown()
throw RuntimeException("Showdown service failed to initialize...")
}
}
}
@@ -51,6 +51,7 @@ class FaintInstruction(battle: PokemonBattle, val message: BattleMessage) : Inte
preamble.thenAccept {
faintingPokemon.effectedPokemon.currentHealth = 0
faintingPokemon.faintedAt = battle.faintCounter++
faintingPokemon.sendUpdate()
CobblemonEvents.BATTLE_FAINTED.post(BattleFaintedEvent(battle, pokemon, context, pnx))
battle.getActorAndActiveSlotFromPNX(pnx).second.battlePokemon = null
@@ -110,6 +110,9 @@ open class BattlePokemon(
/** A set of all the BattlePokemon that they faced during the battle (for exp purposes) */
val facedOpponents = mutableSetOf<BattlePokemon>()
/** The faint sequence number within the battle, or null if this pokemon has not fainted. */
var faintedAt: Int? = null
/**
* The [HeldItemManager] backing this [BattlePokemon].
*/
@@ -151,12 +151,14 @@ class CampfireBlock(settings: Properties, val isSoul: Boolean) : BaseEntityBlock
override fun placeLiquid(level: LevelAccessor, pos: BlockPos, state: BlockState, fluidState: FluidState): Boolean {
val blockEntity = level.getBlockEntity(pos)
if (fluidState.type === Fluids.WATER && blockEntity is CampfireBlockEntity) {
if (!level.isClientSide) {
removePotItem(blockEntity, state, level as Level, pos, null, true)
level.playSoundServer(pos.center, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 1.0F, 1.0F)
}
if (level is Level) {
if (!level.isClientSide) {
removePotItem(blockEntity, state, level, pos, null, true)
level.playSoundServer(pos.center, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 1.0F, 1.0F)
}
if (level.isClientSide) for (i in 0..19) MCCampfireBlock.makeParticles(level as Level, pos, false, true)
if (level.isClientSide) for (i in 0..19) MCCampfireBlock.makeParticles(level, pos, false, true)
}
level.scheduleTick(pos, fluidState.type, fluidState.type.getTickDelay(level))
return true
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.client.gui.battle.subscreen
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.battle.SingleActionRequest
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
@@ -65,7 +66,7 @@ class BattleGeneralActionSelection(
addOption(rank++, battleLang("ui.run"), BattleGUI.runResource) {
CobblemonClient.battle?.minimised = true
Minecraft.getInstance().player?.displayClientMessage(battleLang("run_prompt"), false)
battleGUI.selectAction(request, FleeAttemptActionResponse())
playDownSound(Minecraft.getInstance().soundManager)
}
} else {
@@ -138,8 +138,7 @@ abstract class BattleGimmickButton(gimmick: ShowdownMoveset.Gimmick, val x: Floa
pp = gimmickTemplate?.pp ?: moveTemplate.pp,
priority = gimmickTemplate?.priority ?: moveTemplate.priority,
critRatio = gimmickTemplate?.critRatio ?: moveTemplate.critRatio,
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances,
weight = gimmickTemplate?.weight ?: moveTemplate.weight
effectChances = gimmickTemplate?.effectChances ?: moveTemplate.effectChances
)
}
}
@@ -132,4 +132,4 @@ abstract class InfoTextScrollWidget(val pX: Int, val pY: Int): ScrollingWidget<I
return Component.literal(text)
}
}
}
}
@@ -1,46 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.pokemon.EVs
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.IVs
import com.cobblemon.mod.common.pokemon.Species
data class PokeEditDraft(
var species: Species,
var form: FormData = species.standardForm,
var shiny: Boolean = false,
var gender: Gender = defaultGenderFor(form),
var level: Int = 1,
var originalTrainer: String = "",
var abilityIndex: Int = 0,
var ivs: IVs = IVs(),
var evs: EVs = EVs(),
var cosmeticItem: String? = null,
var cosmeticAspects: Set<String> = emptySet(),
var heldItem: String? = null,
val forcedAspects: MutableSet<String> = mutableSetOf(),
val choiceFeatures: MutableMap<String, String> = mutableMapOf(),
val flagFeatures: MutableSet<String> = mutableSetOf(),
val activeMoves: MutableList<MoveTemplate?> = MutableList(4) { null },
val benchedMoves: MutableSet<MoveTemplate> = mutableSetOf()
) {
companion object {
fun defaultGenderFor(form: FormData): Gender {
return when {
form.maleRatio !in 0F..1F -> Gender.GENDERLESS
form.maleRatio == 0F -> Gender.FEMALE
else -> Gender.MALE
}
}
}
}
@@ -1,600 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.client.gui.ExitButton
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_ICON_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAnimationListWidget
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAnimationTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditEVIVTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditGeneralTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditLocatorListWidget
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditLocatorTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditMovesTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditImplementationBlankTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditWalkSpeedTab
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditSpeciesList
import com.cobblemon.mod.common.client.gui.pokedex.widgets.SearchWidget
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditAction
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.FlagSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
import com.cobblemon.mod.common.pokemon.properties.AspectPropertyType
import com.cobblemon.mod.common.pokemon.OriginalTrainerType
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.gui.components.AbstractWidget
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
class PokeEditGUI private constructor() : Screen(Component.translatable("cobblemon.ui.pokeedit.title")) {
companion object {
private val screenBackground = cobblemonResource("textures/gui/pokedex/pokedex_screen.png")
private val baseBackground = cobblemonResource("textures/gui/pokedex/pokedex_base_red.png")
private val tabSelectArrow = cobblemonResource("textures/gui/pokedex/select_arrow.png")
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
private val tabIcons = arrayOf(
cobblemonResource("textures/gui/pokedex/tab_info.png"),
cobblemonResource("textures/gui/pokedex/tab_stats.png"),
cobblemonResource("textures/gui/pokedex/tab_moves.png"),
cobblemonResource("textures/gui/pokedex/tab_species.png"),
cobblemonResource("textures/gui/pokedex/tab_locations.png"),
)
const val TAB_GENERAL = 0
const val TAB_EV_IV = 1
const val TAB_MOVES = 2
const val TAB_ASPECTS = 3
const val TAB_IMPLEMENTATION_BUTTON = 4
const val IMPL_TAB_LOCATORS = 0
const val IMPL_TAB_ANIMATION = 1
const val IMPL_TAB_SCALE = 2
const val IMPL_TAB_WALK_SPEED = 3
fun open() {
Minecraft.getInstance().setScreen(PokeEditGUI())
}
}
private val allSpecies = PokemonSpecies.species.sortedBy { it.nationalPokedexNumber }
private var filteredSpecies = allSpecies
private lateinit var searchWidget: SearchWidget
private lateinit var speciesList: PokeEditSpeciesList
private lateinit var locatorListWidget: PokeEditLocatorListWidget
private lateinit var animationListWidget: PokeEditAnimationListWidget
private var highlightedLocator: String? = null
private var animationTab: PokeEditAnimationTab? = null
private val tabButtons: MutableList<ScaledButton> = mutableListOf()
private var tabIndex = TAB_GENERAL
private var implementationMode = false
private var implementationTabIndex = IMPL_TAB_LOCATORS
private lateinit var tabElement: AbstractWidget
private lateinit var createButton: SummaryButton
private lateinit var spawnButton: SummaryButton
private lateinit var exitButton: ExitButton
private lateinit var implementationButton: SummaryButton
private lateinit var implementationExitButton: SummaryButton
private val implementationTabButtons: MutableList<SummaryButton> = mutableListOf()
private val draft = PokeEditDraft(allSpecies.firstOrNull() ?: PokemonSpecies.random())
override fun renderBlurredBackground(delta: Float) {}
override fun renderMenuBackground(context: GuiGraphics) {}
override fun isPauseScreen() = false
override fun init() {
super.init()
clearWidgets()
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
if (::searchWidget.isInitialized) removeWidget(searchWidget)
searchWidget = SearchWidget(x + 26, y + 28, 128, 11, update = ::updateFilters)
addRenderableWidget(searchWidget)
if (::speciesList.isInitialized) removeWidget(speciesList)
speciesList = PokeEditSpeciesList(x + 26, y + 39, ::setSelectedSpecies)
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
locatorListWidget = PokeEditLocatorListWidget(
pX = x + 26,
pY = y + 190,
listWidth = 139,
listHeight = 151,
onSelectLocator = { name ->
highlightedLocator = name
(tabElement as? PokeEditLocatorTab)?.setHighlightedLocator(name)
}
)
locatorListWidget.setLocators(emptyList())
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
animationListWidget = PokeEditAnimationListWidget(
pX = x + 26,
pY = y + 190,
listWidth = 139,
listHeight = 151,
isPlaying = { name -> animationTab?.isAnimationPlaying(name) == true },
isLooping = { name -> animationTab?.isAnimationLooping(name) == true },
getDuration = { name -> animationTab?.getAnimationDuration(name) ?: 0F },
getProgress = { name -> animationTab?.getAnimationProgress(name) ?: 0F },
onPlay = { name -> animationTab?.playAnimation(name) },
onPause = { name -> animationTab?.pauseAnimation(name) },
onToggleLoop = { name, enabled -> animationTab?.setAnimationLooping(name, enabled) }
)
animationListWidget.setAnimations(emptyList())
setUpTabs()
setUpActionButtons()
setUpImplementationControls()
setImplementationMode(implementationMode, force = true)
updateFilters(true)
if (implementationMode) {
displayImplementationTab(implementationTabIndex, recreate = true)
} else {
displayTab(tabIndex, recreate = true)
}
}
override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
renderBackground(context, mouseX, mouseY, delta)
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
blitk(
matrixStack = matrices,
texture = baseBackground,
x = x,
y = y,
width = BASE_WIDTH,
height = BASE_HEIGHT
)
blitk(
matrixStack = matrices,
texture = screenBackground,
x = x,
y = y,
width = BASE_WIDTH,
height = BASE_HEIGHT
)
drawScaledText(
context = context,
text = lang("ui.pokeedit.title"),
x = x + 192,
y = y + 15,
shadow = true
)
super.render(context, mouseX, mouseY, delta)
tabButtons.forEach { it.render(context, mouseX, mouseY, delta) }
if (!implementationMode) {
// Draw the tab arrow above overlay widgets so it stays visible.
blitk(
matrixStack = matrices,
texture = tabSelectArrow,
x = (x + 198 + (25 * tabIndex)) / SCALE,
y = (y + 177) / SCALE,
width = 12,
height = 6,
scale = SCALE
)
}
}
private fun updateFilters(init: Boolean = false) {
val search = searchWidget.value.trim()
filteredSpecies = if (search.isEmpty()) {
allSpecies
} else {
allSpecies.filter { species ->
species.translatedName.string.contains(search, true)
|| species.resourceIdentifier.path.contains(search, true)
}
}
speciesList.setEntries(filteredSpecies)
if (init || draft.species !in filteredSpecies) {
filteredSpecies.firstOrNull()?.let { setSelectedSpecies(it) }
}
}
private fun setSelectedSpecies(species: com.cobblemon.mod.common.pokemon.Species) {
if (draft.species == species) return
draft.species = species
draft.form = species.standardForm
draft.gender = PokeEditDraft.defaultGenderFor(draft.form)
draft.shiny = false
draft.abilityIndex = 0
val aspectOptions = PokeEditAspectsTab.collectAspectOptions(
species,
draft.form,
draft.gender,
draft.shiny
)
draft.forcedAspects.retainAll(aspectOptions.toggles)
draft.flagFeatures.retainAll(aspectOptions.flagKeys)
draft.choiceFeatures.entries.removeIf { (key, choice) ->
val group = aspectOptions.choiceGroups.firstOrNull { it.key == key } ?: return@removeIf true
choice !in group.choices
}
val cosmetic = aspectOptions.cosmeticChoices.firstOrNull { it.itemId.toString() == draft.cosmeticItem }
draft.cosmeticItem = cosmetic?.itemId?.toString()
draft.cosmeticAspects = cosmetic?.aspects?.toSet() ?: emptySet()
if (implementationMode) {
displayImplementationTab(implementationTabIndex, recreate = true)
} else {
displayTab(tabIndex, recreate = true)
}
}
private fun setUpTabs() {
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
tabButtons.clear()
for (i in tabIcons.indices) {
tabButtons.add(
ScaledButton(
x + 197F + (i * 25F),
y + 181.5F,
TAB_ICON_SIZE,
TAB_ICON_SIZE,
resource = tabIcons[i],
clickAction = {
if (i == TAB_IMPLEMENTATION_BUTTON) {
setImplementationMode(true)
} else {
displayTab(i)
}
}
)
)
}
tabButtons.forEach(::addRenderableWidget)
}
private fun setUpActionButtons() {
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val actionY = y + 200F
val exitX = x + 312F
val spawnX = exitX - 56F
val createX = spawnX - 56F
val implX = x + 26F
if (::createButton.isInitialized) removeWidget(createButton)
createButton = SummaryButton(
buttonX = createX,
buttonY = actionY,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { sendPokeEditAction(PokeEditAction.CREATE) },
text = lang("ui.pokeedit.give"),
resource = actionButtonResource
)
addRenderableWidget(createButton)
if (::spawnButton.isInitialized) removeWidget(spawnButton)
spawnButton = SummaryButton(
buttonX = spawnX,
buttonY = actionY,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { sendPokeEditAction(PokeEditAction.SPAWN) },
text = lang("ui.pokeedit.spawn"),
resource = actionButtonResource
)
addRenderableWidget(spawnButton)
if (::exitButton.isInitialized) removeWidget(exitButton)
exitButton = ExitButton(pX = exitX.toInt(), pY = y + 200) {
Minecraft.getInstance().setScreen(null)
}
addRenderableWidget(exitButton)
if (::implementationButton.isInitialized) removeWidget(implementationButton)
implementationButton = SummaryButton(
buttonX = implX,
buttonY = actionY,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { setImplementationMode(true) },
text = Component.literal("Impl"),
resource = actionButtonResource,
renderRequirement = { !implementationMode },
clickRequirement = { !implementationMode },
largeText = false,
boldText = false,
textScale = 0.7F
)
addRenderableWidget(implementationButton)
}
private fun setUpImplementationControls() {
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val sideX = (x - 60F).coerceAtLeast(6F)
if (::implementationExitButton.isInitialized) removeWidget(implementationExitButton)
implementationExitButton = SummaryButton(
buttonX = sideX,
buttonY = y + 28F,
buttonWidth = 54,
buttonHeight = 15,
clickAction = { setImplementationMode(false) },
text = Component.literal("Back"),
resource = actionButtonResource,
renderRequirement = { implementationMode },
clickRequirement = { implementationMode },
largeText = false,
boldText = false,
textScale = 0.7F
)
addRenderableWidget(implementationExitButton)
implementationTabButtons.forEach(::removeWidget)
implementationTabButtons.clear()
val baseY = y + 48F
val labels = listOf(
Component.literal("Locators"),
Component.literal("Animation"),
Component.literal("Scale"),
Component.literal("Walk Speed")
)
labels.forEachIndexed { index, label ->
val button = SummaryButton(
buttonX = sideX,
buttonY = baseY + (index * 18F),
buttonWidth = 54,
buttonHeight = 15,
clickAction = { displayImplementationTab(index) },
text = label,
resource = actionButtonResource,
renderRequirement = { implementationMode },
clickRequirement = { implementationMode },
largeText = false,
boldText = false,
textScale = 0.7F
)
implementationTabButtons.add(button)
addRenderableWidget(button)
}
}
private fun displayTab(newIndex: Int, recreate: Boolean = false) {
if (implementationMode) {
return
}
if (tabIndex == newIndex && ::tabElement.isInitialized && !recreate) {
return
}
tabIndex = newIndex
if (::tabElement.isInitialized) removeWidget(tabElement)
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val tabX = x + 180
val tabY = y + 28
tabElement = when (newIndex) {
TAB_GENERAL -> PokeEditGeneralTab(tabX, tabY, draft) { displayTab(tabIndex, recreate = true) }
TAB_EV_IV -> PokeEditEVIVTab(tabX, tabY, draft)
TAB_MOVES -> PokeEditMovesTab(tabX, tabY, draft)
TAB_ASPECTS -> PokeEditAspectsTab(tabX, tabY, draft)
else -> PokeEditGeneralTab(tabX, tabY, draft) { displayTab(tabIndex, recreate = true) }
}
addRenderableWidget(tabElement)
bringControlsToFront()
}
private fun displayImplementationTab(newIndex: Int, recreate: Boolean = false) {
if (!implementationMode) {
return
}
if (implementationTabIndex == newIndex && ::tabElement.isInitialized && !recreate) {
return
}
implementationTabIndex = newIndex
if (::tabElement.isInitialized) removeWidget(tabElement)
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
animationTab = null
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val tabX = x + 180
val tabY = y + 28
tabElement = when (newIndex) {
IMPL_TAB_LOCATORS -> PokeEditLocatorTab(
tabX,
tabY,
draft,
highlightedLocator,
::updateLocatorList
)
IMPL_TAB_ANIMATION -> PokeEditAnimationTab(tabX, tabY, draft) { animations ->
animationListWidget.setAnimations(animations)
}
IMPL_TAB_SCALE -> PokeEditImplementationBlankTab(tabX, tabY, draft, Component.literal("Scale"))
IMPL_TAB_WALK_SPEED -> PokeEditWalkSpeedTab(tabX, tabY, draft)
else -> PokeEditLocatorTab(
tabX,
tabY,
draft,
highlightedLocator,
::updateLocatorList
)
}
addRenderableWidget(tabElement)
when (newIndex) {
IMPL_TAB_LOCATORS -> addRenderableWidget(locatorListWidget)
IMPL_TAB_ANIMATION -> {
animationTab = tabElement as? PokeEditAnimationTab
animationListWidget.setAnimations(animationTab?.getAnimations().orEmpty())
addRenderableWidget(animationListWidget)
}
}
bringControlsToFront()
}
private fun setImplementationMode(enabled: Boolean, force: Boolean = false) {
if (!force && implementationMode == enabled) return
implementationMode = enabled
if (implementationMode) {
if (::searchWidget.isInitialized) removeWidget(searchWidget)
if (::speciesList.isInitialized) removeWidget(speciesList)
implementationTabIndex = IMPL_TAB_LOCATORS
displayImplementationTab(implementationTabIndex, recreate = true)
} else {
if (::searchWidget.isInitialized) addRenderableWidget(searchWidget)
if (::speciesList.isInitialized) addRenderableWidget(speciesList)
if (::locatorListWidget.isInitialized) removeWidget(locatorListWidget)
if (::animationListWidget.isInitialized) removeWidget(animationListWidget)
displayTab(tabIndex, recreate = true)
}
tabButtons.forEach { it.isVisible = !implementationMode }
if (!implementationMode) {
searchWidget.isFocused = false
}
bringControlsToFront()
}
private fun bringControlsToFront() {
if (::createButton.isInitialized) {
removeWidget(createButton)
addRenderableWidget(createButton)
}
if (::spawnButton.isInitialized) {
removeWidget(spawnButton)
addRenderableWidget(spawnButton)
}
if (::exitButton.isInitialized) {
removeWidget(exitButton)
addRenderableWidget(exitButton)
}
if (::implementationButton.isInitialized) {
removeWidget(implementationButton)
addRenderableWidget(implementationButton)
}
if (tabButtons.isEmpty()) return
tabButtons.forEach { removeWidget(it) }
tabButtons.forEach(::addRenderableWidget)
}
private fun sendPokeEditAction(action: PokeEditAction) {
val properties = buildDraftProperties()
val moveNames = draft.activeMoves.map { it?.name ?: "" }
val cosmeticItem = draft.cosmeticItem?.trim().orEmpty()
PokeEditActionPacket(action, properties.asString(), moveNames, cosmeticItem).sendToServer()
Minecraft.getInstance().setScreen(null)
}
private fun updateLocatorList(locators: Collection<String>) {
if (::locatorListWidget.isInitialized) {
locatorListWidget.setLocators(locators)
}
}
private fun buildDraftProperties(): PokemonProperties {
val props = PokemonProperties()
props.species = draft.species.resourceIdentifier.toString()
props.form = draft.form.formOnlyShowdownId()
props.shiny = draft.shiny
props.gender = draft.gender
props.level = draft.level
val ot = draft.originalTrainer.trim()
if (ot.isBlank()) {
props.originalTrainerType = OriginalTrainerType.NONE
} else {
props.originalTrainerType = OriginalTrainerType.NPC
props.originalTrainer = ot
}
props.heldItem = draft.heldItem?.takeIf { it.isNotBlank() }
props.ivs = draft.ivs
props.evs = draft.evs
val abilities = draft.form.abilities.map { it.template }
props.ability = abilities.getOrNull(draft.abilityIndex)?.name
val customProperties = mutableListOf<com.cobblemon.mod.common.api.properties.CustomPokemonProperty>()
draft.forcedAspects.mapNotNullTo(customProperties) { aspect -> AspectPropertyType.fromString(aspect) }
draft.flagFeatures.forEach { key ->
val provider = SpeciesFeatures.getFeature(key) as? FlagSpeciesFeatureProvider ?: return@forEach
provider.fromString("true")?.let { customProperties.add(it) }
}
draft.choiceFeatures.forEach { (key, choice) ->
val provider = SpeciesFeatures.getFeature(key) as? ChoiceSpeciesFeatureProvider ?: return@forEach
provider.fromString(choice)?.let { customProperties.add(it) }
}
props.customProperties = customProperties
props.updateAspects()
return props
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (focused !is EditBox && (minecraft?.options?.keyInventory?.matches(keyCode, scanCode) == true)) {
onClose()
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
return try {
super.mouseClicked(mouseX, mouseY, button)
} catch (_: ConcurrentModificationException) {
false
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (implementationMode) {
if (implementationTabIndex == IMPL_TAB_LOCATORS && ::locatorListWidget.isInitialized) {
if (locatorListWidget.mouseScrolled(mouseX, mouseY, amount, verticalAmount)) return true
}
if (implementationTabIndex == IMPL_TAB_ANIMATION && ::animationListWidget.isInitialized) {
if (animationListWidget.mouseScrolled(mouseX, mouseY, amount, verticalAmount)) return true
}
}
return super.mouseScrolled(mouseX, mouseY, amount, verticalAmount)
}
}
@@ -1,100 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.abilities.AbilityTemplate
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.pokedex.ScaledButton
import com.cobblemon.mod.common.client.gui.pokedex.widgets.InfoTextScrollWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
class PokeEditAbilityTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, 139, 163, lang("ui.pokeedit.abilities")) {
companion object {
private val arrowLeft = cobblemonResource("textures/gui/pokedex/info_arrow_left.png")
private val arrowRight = cobblemonResource("textures/gui/pokedex/info_arrow_right.png")
}
private val description = object : InfoTextScrollWidget(pX, pY + 60) {}
private val leftButton = ScaledButton(
pX + 2.5F,
pY + 30F,
7,
10,
arrowLeft,
clickAction = { switchAbility(false) }
).also { addWidget(it) }
private val rightButton = ScaledButton(
pX + 133F,
pY + 30F,
7,
10,
arrowRight,
clickAction = { switchAbility(true) }
).also { addWidget(it) }
private var abilitiesList: List<AbilityTemplate> = emptyList()
init {
refreshAbilities()
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
refreshAbilities()
if (abilitiesList.isEmpty()) return
val ability = abilitiesList[draft.abilityIndex.coerceIn(0, abilitiesList.lastIndex)]
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = Component.translatable("cobblemon.ui.pokedex.info.ability", ability.displayName.asTranslated()).bold(),
x = x + 9,
y = y + 30,
shadow = true
)
description.setText(listOf(ability.description))
description.render(context, mouseX, mouseY, delta)
leftButton.render(context, mouseX, mouseY, delta)
rightButton.render(context, mouseX, mouseY, delta)
}
private fun refreshAbilities() {
val form = draft.form
abilitiesList = form.abilities.map { it.template }
if (draft.abilityIndex !in abilitiesList.indices) {
draft.abilityIndex = 0
}
}
private fun switchAbility(nextIndex: Boolean) {
if (abilitiesList.isEmpty()) return
val size = abilitiesList.size
draft.abilityIndex = if (nextIndex) {
(draft.abilityIndex + 1) % size
} else {
(draft.abilityIndex - 1 + size) % size
}
}
}
@@ -1,362 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
import java.util.Locale
class PokeEditAnimationListWidget(
val pX: Int,
val pY: Int,
val listWidth: Int,
val listHeight: Int,
private val isPlaying: (String) -> Boolean,
private val isLooping: (String) -> Boolean,
private val getDuration: (String) -> Float,
private val getProgress: (String) -> Float,
private val onPlay: (String) -> Unit,
private val onPause: (String) -> Unit,
private val onToggleLoop: (String, Boolean) -> Unit
) : ScrollingWidget<PokeEditAnimationListWidget.AnimationRow>(
left = pX,
top = pY - listHeight,
width = listWidth,
height = listHeight,
slotHeight = ROW_HEIGHT
) {
companion object {
private const val ROW_HEIGHT = 12
private const val TEXT_SCALE = 0.5F
private const val BORDER_COLOR = 0xFF1B1B1B.toInt()
private const val FILL_COLOR = 0xFF3B3B3B.toInt()
private const val TEXT_COLOR = 0xFFFCFCFC.toInt()
private const val BUTTON_WIDTH = 12
private const val BUTTON_HEIGHT = 9
private const val BUTTON_GAP = 3
private const val BUTTON_PAD_RIGHT = 4
private const val BUTTON_PAD_TOP = 1
private const val BUTTON_ACTIVE = 0xFF6BD46B.toInt()
private const val BUTTON_INACTIVE = 0xFF5B5B5B.toInt()
private const val BUTTON_BORDER = 0xFF1B1B1B.toInt()
private const val LOOP_BOX_SIZE = 8
private const val LOOP_GAP = 3
private const val LOOP_LABEL_SCALE = 0.45F
private const val PROGRESS_HEIGHT = 2
private const val PROGRESS_BG = 0xFF2C2C2C.toInt()
private const val PROGRESS_FG = 0xFF8AD5FF.toInt()
private const val DURATION_SCALE = 0.45F
private const val DURATION_WIDTH = 28
}
private val listTop = pY - listHeight
private val listBottom = pY
private var draggingScrollbar = false
fun setAnimations(animations: Collection<String>) {
clearEntries()
animations.sorted().forEach { addEntry(AnimationRow(it)) }
scrollAmount = 0.0
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.fill(pX - 1, listTop - 1, pX + listWidth + 1, listBottom + 1, BORDER_COLOR)
context.fill(pX, listTop, pX + listWidth, listBottom, FILL_COLOR)
focused = if (mouseX in pX..(pX + listWidth) && mouseY in listTop..listBottom) {
getEntryAtPosition(mouseX.toDouble(), mouseY.toDouble())
} else {
null
}
context.enableScissor(pX, listTop, pX + listWidth, listBottom)
renderListItems(context, mouseX, mouseY, delta)
context.disableScissor()
if (maxScroll > 0) {
renderScrollbar(context, mouseX, mouseY, delta)
}
}
override fun renderListItems(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val rowX = rowLeft
for (index in 0 until itemCount) {
val rowY = getRowTop(index)
val rowBottom = getRowBottom(index)
if (rowBottom >= listTop && rowY <= listBottom) {
val entry = getEntry(index)
if (entry is AnimationRow) {
entry.isPlaying = isPlaying(entry.name)
entry.isLooping = isLooping(entry.name)
entry.durationSeconds = getDuration(entry.name)
entry.progress = getProgress(entry.name)
}
renderItem(context, mouseX, mouseY, delta, index, rowX, rowY, rowWidth, itemHeight)
}
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
) {
return false
}
val maxScroll = (itemCount * itemHeight - listHeight).coerceAtLeast(0)
if (maxScroll <= 0) return true
val step = if (verticalAmount != 0.0) verticalAmount else amount
scrollAmount = Mth.clamp(scrollAmount - step * itemHeight, 0.0, maxScroll.toDouble())
return true
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
|| button != 0
) {
return false
}
if (isOverScrollbar(mouseX, mouseY) && maxScroll > 0) {
draggingScrollbar = true
return true
}
val localY = mouseY.toInt() - listTop + scrollAmount.toInt()
if (localY < 0) return false
val index = localY / itemHeight
if (index !in 0 until itemCount) return false
val entry = getEntry(index)
if (entry is AnimationRow) {
val rowX = rowLeft
val rowY = getRowTop(index)
val (playX, pauseX, loopX, buttonY, durationX) = buttonPositions(rowX, rowY)
val inPlay = mouseX.toInt() in playX..(playX + BUTTON_WIDTH)
&& mouseY.toInt() in buttonY..(buttonY + BUTTON_HEIGHT)
val inPause = mouseX.toInt() in pauseX..(pauseX + BUTTON_WIDTH)
&& mouseY.toInt() in buttonY..(buttonY + BUTTON_HEIGHT)
val inLoop = mouseX.toInt() in loopX..(loopX + LOOP_BOX_SIZE)
&& mouseY.toInt() in buttonY..(buttonY + LOOP_BOX_SIZE)
if (inPlay) {
onPlay(entry.name)
return true
}
if (inPause) {
onPause(entry.name)
return true
}
if (inLoop) {
val newValue = !isLooping(entry.name)
onToggleLoop(entry.name, newValue)
return true
}
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (button == 0 && draggingScrollbar) {
draggingScrollbar = false
return true
}
return super.mouseReleased(mouseX, mouseY, button)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (!draggingScrollbar || maxScroll <= 0) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
val barHeight = listBottom - listTop
var thumbHeight = ((barHeight * barHeight).toFloat() / maxPosition.toFloat()).toInt()
thumbHeight = Mth.clamp(thumbHeight, 16, barHeight - 4)
val trackHeight = barHeight - thumbHeight
if (trackHeight <= 0) return true
val clampedY = Mth.clamp(mouseY.toInt() - listTop - thumbHeight / 2, 0, trackHeight)
scrollAmount = (clampedY.toDouble() * maxScroll.toDouble()) / trackHeight.toDouble()
return true
}
override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
return mouseX in pX.toDouble()..(pX + listWidth).toDouble()
&& mouseY in listTop.toDouble()..listBottom.toDouble()
}
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val yStart = listTop
val barHeight = listBottom - listTop
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 16, barHeight - 4)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
if (yTop < yStart) yTop = yStart
context.fill(xLeft + 1, yStart, xRight - 1, listBottom, 0xFF7AEEF1.toInt())
context.fill(xLeft, yTop, xRight, yTop + yBottom, 0xFF3A96B6.toInt())
}
override fun getRowTop(index: Int): Int {
return listTop - scrollAmount.toInt() + (index * itemHeight)
}
override fun getRowBottom(index: Int): Int {
return getRowTop(index) + itemHeight
}
override fun getBottom(): Int {
return listBottom
}
private fun isOverScrollbar(mouseX: Double, mouseY: Double): Boolean {
val xLeft = scrollbarPosition
val xRight = xLeft + 3
return mouseX >= xLeft && mouseX <= xRight && mouseY >= listTop && mouseY <= listBottom
}
private fun buttonPositions(rowX: Int, rowY: Int): List<Int> {
val loopX = rowX + listWidth - BUTTON_PAD_RIGHT - LOOP_BOX_SIZE
val pauseX = loopX - LOOP_GAP - BUTTON_WIDTH
val playX = pauseX - BUTTON_GAP - BUTTON_WIDTH
val buttonY = rowY + BUTTON_PAD_TOP
val durationX = playX - BUTTON_GAP - DURATION_WIDTH
return listOf(playX, pauseX, loopX, buttonY, durationX)
}
class AnimationRow(val name: String) : Slot<AnimationRow>() {
var isPlaying: Boolean = false
var isLooping: Boolean = false
var durationSeconds: Float = 0F
var progress: Float = 0F
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
val bg = if (hovered) 0xFF2F2F2F.toInt() else null
if (bg != null) {
context.fill(x + 1, y, x + entryWidth - 1, y + ROW_HEIGHT, bg)
}
val loopX = x + entryWidth - BUTTON_PAD_RIGHT - LOOP_BOX_SIZE
val pauseX = loopX - LOOP_GAP - BUTTON_WIDTH
val playX = pauseX - BUTTON_GAP - BUTTON_WIDTH
val durationX = playX - BUTTON_GAP - DURATION_WIDTH
val buttonY = y + BUTTON_PAD_TOP
drawButton(context, playX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, !isPlaying)
drawPlayIcon(context, playX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT)
drawButton(context, pauseX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, isPlaying)
drawPauseIcon(context, pauseX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT)
drawLoopBox(context, loopX, buttonY, isLooping)
drawDuration(context, durationX, buttonY, durationSeconds)
drawScaledText(
context = context,
text = name.text(),
x = x + 3,
y = y + 2,
scale = TEXT_SCALE,
colour = TEXT_COLOR,
shadow = false
)
drawProgressBar(context, x + 2, y + ROW_HEIGHT - PROGRESS_HEIGHT, entryWidth - 4, progress)
}
override fun getNarration(): Component {
return Component.literal(name)
}
private fun drawButton(
context: GuiGraphics,
x: Int,
y: Int,
width: Int,
height: Int,
active: Boolean
) {
val fill = if (active) BUTTON_ACTIVE else BUTTON_INACTIVE
context.fill(x - 1, y - 1, x + width + 1, y + height + 1, BUTTON_BORDER)
context.fill(x, y, x + width, y + height, fill)
}
private fun drawLoopBox(context: GuiGraphics, x: Int, y: Int, enabled: Boolean) {
val fill = if (enabled) BUTTON_ACTIVE else BUTTON_INACTIVE
context.fill(x - 1, y - 1, x + LOOP_BOX_SIZE + 1, y + LOOP_BOX_SIZE + 1, BUTTON_BORDER)
context.fill(x, y, x + LOOP_BOX_SIZE, y + LOOP_BOX_SIZE, fill)
drawLoopIcon(context, x, y, LOOP_BOX_SIZE)
}
private fun drawPlayIcon(context: GuiGraphics, x: Int, y: Int, width: Int, height: Int) {
val iconWidth = 6
val iconHeight = 7
val startX = x + (width - iconWidth) / 2
val startY = y + (height - iconHeight) / 2
for (i in 0 until iconWidth) {
val top = startY + i / 2
val bottom = startY + iconHeight - i / 2
context.fill(startX + i, top, startX + i + 1, bottom, TEXT_COLOR)
}
}
private fun drawPauseIcon(context: GuiGraphics, x: Int, y: Int, width: Int, height: Int) {
val barWidth = 2
val barHeight = 7
val gap = 2
val startX = x + (width - (barWidth * 2 + gap)) / 2
val startY = y + (height - barHeight) / 2
context.fill(startX, startY, startX + barWidth, startY + barHeight, TEXT_COLOR)
context.fill(startX + barWidth + gap, startY, startX + barWidth * 2 + gap, startY + barHeight, TEXT_COLOR)
}
private fun drawLoopIcon(context: GuiGraphics, x: Int, y: Int, size: Int) {
val startX = x + 1
val startY = y + 1
val endX = x + size - 1
val endY = y + size - 1
context.fill(startX, startY, endX, startY + 1, TEXT_COLOR)
context.fill(startX, endY - 1, endX, endY, TEXT_COLOR)
context.fill(startX, startY, startX + 1, endY, TEXT_COLOR)
context.fill(endX - 1, startY, endX, endY, TEXT_COLOR)
context.fill(endX - 2, startY, endX, startY + 2, TEXT_COLOR)
}
private fun drawDuration(context: GuiGraphics, x: Int, y: Int, duration: Float) {
val text = String.format(Locale.US, "%.2fs", duration.coerceAtLeast(0F))
drawScaledText(
context = context,
text = text.text(),
x = x + DURATION_WIDTH / 2,
y = y + 2,
scale = DURATION_SCALE,
colour = TEXT_COLOR,
maxCharacterWidth = DURATION_WIDTH,
centered = true,
shadow = false
)
}
private fun drawProgressBar(context: GuiGraphics, x: Int, y: Int, width: Int, progress: Float) {
val clamped = Mth.clamp(progress, 0F, 1F)
context.fill(x, y, x + width, y + PROGRESS_HEIGHT, PROGRESS_BG)
context.fill(x, y, x + (width * clamped).toInt(), y + PROGRESS_HEIGHT, PROGRESS_FG)
}
}
}
@@ -1,496 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
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.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.animation.ActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PrimaryAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.pose.Pose
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.util.toHex
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.platform.Lighting
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.client.Minecraft
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import org.joml.Quaternionf
import org.joml.Vector3f
class PokeEditAnimationTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft,
private val onAnimationsChanged: (List<String>) -> Unit
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, Component.literal("Animation")) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
val held = currentHeldStack()
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE, PoseType.STAND, PoseType.WALK, PoseType.SLEEP)
private var selectedPoseIndex: Int = 0
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
private var availableAnimations: List<String> = emptyList()
private val activeAnimationsByName = mutableMapOf<String, ActiveAnimation>()
private val loopingAnimations = mutableMapOf<String, Boolean>()
private val animationStartTimes = mutableMapOf<String, Float>()
private val animationDurations = mutableMapOf<String, Float>()
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = context.pose(),
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
renderPokemon(context, delta)
refreshAnimationList()
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
fun getAnimations(): List<String> = availableAnimations
fun isAnimationPlaying(name: String): Boolean = activeAnimationsByName.containsKey(name)
fun isAnimationLooping(name: String): Boolean = loopingAnimations[name] == true
fun getAnimationDuration(name: String): Float {
return animationDurations[name] ?: resolveDuration(name).also { animationDurations[name] = it }
}
fun getAnimationProgress(name: String): Float {
val duration = getAnimationDuration(name)
if (duration <= 0F) return 0F
val start = animationStartTimes[name] ?: return 0F
val elapsed = state.animationSeconds - start
val raw = if (isAnimationLooping(name)) {
(elapsed % duration) / duration
} else {
elapsed / duration
}
return raw.coerceIn(0F, 1F)
}
fun playAnimation(name: String) {
if (activeAnimationsByName.containsKey(name)) return
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
val animation = model.getAnimation(state, name, state.runtime)
val active = if (animation != null) {
if (animation is PrimaryAnimation) animation.animation else animation
} else {
val pose = resolvePoseAnimation(model, name) ?: return
PoseActiveAnimation(pose)
}
state.addActiveAnimation(active)
activeAnimationsByName[name] = active
animationStartTimes[name] = state.animationSeconds
animationDurations[name] = active.duration
}
fun pauseAnimation(name: String) {
val active = activeAnimationsByName.remove(name) ?: return
state.activeAnimations.remove(active)
animationStartTimes.remove(name)
}
fun setAnimationLooping(name: String, enabled: Boolean) {
loopingAnimations[name] = enabled
}
private fun refreshAnimationList() {
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
val names = mutableSetOf<String>()
names.addAll(model.animations.keys)
model.poses.values.forEach { pose ->
names.addAll(pose.namedAnimations.keys)
names.add(pose.poseName)
}
val sorted = names.sorted()
if (sorted != availableAnimations) {
availableAnimations = sorted
loopingAnimations.keys.retainAll(sorted.toSet())
animationDurations.keys.retainAll(sorted.toSet())
animationStartTimes.keys.retainAll(sorted.toSet())
onAnimationsChanged(sorted)
}
}
private fun renderPokemon(context: GuiGraphics, delta: Float) {
buildRenderable()?.let { renderable ->
state.currentAspects = renderable.aspects
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
val matrices = context.pose()
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
renderAnimationModel(context, renderable, delta)
if (!renderable.heldItem.isEmpty) {
heldItemRenderer.renderOnModel(
renderable.heldItem,
state,
matrices,
context.bufferSource(),
light = 0xF000F0,
frontLight = true
)
}
matrices.popPose()
context.disableScissor()
}
}
private fun renderAnimationModel(context: GuiGraphics, renderable: RenderablePokemon, delta: Float) {
val speciesId = renderable.species.resourceIdentifier
val model = VaryingModelRepository.getPoser(speciesId, state)
val texture = VaryingModelRepository.getTexture(speciesId, state)
val renderContext = RenderContext()
model.context = renderContext
VaryingModelRepository.getTextureNoSubstitute(speciesId, state).let { renderContext.put(RenderContext.TEXTURE, it) }
val baseScale = PokemonSpecies.getByIdentifier(speciesId)!!.getForm(state.currentAspects).baseScale
renderContext.put(RenderContext.SCALE, baseScale)
renderContext.put(RenderContext.SPECIES, speciesId)
renderContext.put(RenderContext.ASPECTS, state.currentAspects)
renderContext.put(RenderContext.RENDER_STATE, RenderContext.RenderState.PROFILE)
renderContext.put(RenderContext.POSABLE_STATE, state)
renderContext.put(RenderContext.DO_QUIRKS, false)
state.currentModel = model
if (state.currentPose == null) {
state.currentPose = model.getPose(PoseType.PROFILE)?.poseName
}
state.updatePartialTicks(delta)
RenderSystem.applyModelViewMatrix()
val matrices = context.pose()
val rotationVector = Vector3f(13F, rotationY, 0F)
val rotation = Quaternionf().fromEulerXYZDegrees(rotationVector)
matrices.pushPose()
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
matrices.translate(
model.profileTranslation.x,
model.profileTranslation.y + 1.5 * model.profileScale,
model.profileTranslation.z - 4.0
)
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
matrices.mulPose(rotation)
model.setDefault()
state.preRender()
state.renderMarkers.clear()
runActiveAnimations(renderContext, model)
model.updateLocators(null, state)
Lighting.setupForEntityInInventory()
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
val buffer = bufferSource.getBuffer(RenderType.entityCutout(texture))
val light1 = Vector3f(-1F, 1F, 1.0F)
val light2 = Vector3f(1.3F, -1F, 1.0F)
RenderSystem.setShaderLights(light1, light2)
val packedLight = LightTexture.pack(11, 7)
val colour = toHex(1F, 1F, 1F, 1F)
model.withLayerContext(bufferSource, state, VaryingModelRepository.getLayers(speciesId, state)) {
model.render(renderContext, matrices, buffer, packedLight, OverlayTexture.NO_OVERLAY, colour)
bufferSource.endBatch()
}
model.setDefault()
Lighting.setupFor3DItems()
matrices.popPose()
}
private fun runActiveAnimations(context: RenderContext, model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel) {
val toRestart = mutableListOf<String>()
val iterator = activeAnimationsByName.entries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
val animation = entry.value
val stillRunning = animation.run(
context,
model,
state,
0F,
0F,
state.animationSeconds,
0F,
0F,
1F
)
if (!stillRunning) {
state.activeAnimations.remove(animation)
iterator.remove()
animationStartTimes.remove(entry.key)
if (loopingAnimations[entry.key] == true) {
toRestart.add(entry.key)
}
}
}
if (toRestart.isNotEmpty()) {
toRestart.forEach { name ->
val animation = model.getAnimation(state, name, state.runtime)
val active = if (animation != null) {
if (animation is PrimaryAnimation) animation.animation else animation
} else {
val pose = resolvePoseAnimation(model, name) ?: return@forEach
PoseActiveAnimation(pose)
}
state.addActiveAnimation(active)
activeAnimationsByName[name] = active
animationStartTimes[name] = state.animationSeconds
animationDurations[name] = active.duration
}
}
}
private fun resolvePoseAnimation(
model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel,
name: String
): Pose? {
return model.poses[name] ?: model.poses.values.firstOrNull { it.poseName.equals(name, ignoreCase = true) }
}
private fun resolveDuration(name: String): Float {
val model = VaryingModelRepository.getPoser(draft.species.resourceIdentifier, state)
val animation = model.getAnimation(state, name, state.runtime)
val active = if (animation != null) {
if (animation is PrimaryAnimation) animation.animation else animation
} else {
return 0F
}
return active.duration
}
private class PoseActiveAnimation(private val pose: Pose) : ActiveAnimation {
override val isTransition: Boolean = false
override val duration: Float = 0F
override val enduresPrimaryAnimations: Boolean = true
override fun run(
context: RenderContext,
model: com.cobblemon.mod.common.client.render.models.blockbench.PosableModel,
state: com.cobblemon.mod.common.client.render.models.blockbench.PosableState,
limbSwing: Float,
limbSwingAmount: Float,
ageInTicks: Float,
headYaw: Float,
headPitch: Float,
intensity: Float
): Boolean {
if (!pose.isSuitable(state)) return true
pose.apply(context, model, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch)
return true
}
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = draft.heldItem
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
val prev = renderablePokemon
if (prev == null || prev.species != species || prev.aspects != aspects) {
activeAnimationsByName.clear()
state.activeAnimations.clear()
}
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun currentHeldStack(): ItemStack {
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -1,620 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.api.conditional.RegistryLikeIdentifierCondition
import com.cobblemon.mod.common.api.conditional.RegistryLikeTagCondition
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.FlagSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.substitute
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.language.I18n
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.util.Mth
class PokeEditAspectsTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.aspects")) {
companion object {
private const val WIDTH = 139
private const val HEIGHT = 163
private const val SLOT_HEIGHT = 16
private const val SCALE = 0.7F
private const val TOP_MARGIN = 10
private const val BOTTOM_MARGIN = 10
private const val COLUMN_GAP = 6
data class ChoiceGroup(
val key: String,
val choices: List<String>,
val aspectFormat: String
)
data class CosmeticChoice(
val itemId: ResourceLocation,
val aspects: List<String>,
val label: MutableComponent
)
data class AspectOptions(
val toggles: Set<String>,
val choiceGroups: List<ChoiceGroup>,
val flagKeys: Set<String>,
val cosmeticChoices: List<CosmeticChoice>
)
fun collectAspectOptions(
species: Species,
form: FormData? = null,
gender: Gender? = null,
shiny: Boolean? = null
): AspectOptions {
val toggles = mutableSetOf<String>()
val choiceGroups = mutableListOf<ChoiceGroup>()
val flagKeys = mutableSetOf<String>()
val forms = if (species.forms.isEmpty()) listOf(species.standardForm) else species.forms
forms.forEach { form -> toggles.addAll(form.aspects) }
SpeciesFeatures.getFeaturesFor(species).forEach { provider ->
when (provider) {
is ChoiceSpeciesFeatureProvider -> {
if (provider.isAspect && provider.keys.isNotEmpty() && provider.choices.isNotEmpty()) {
choiceGroups.add(ChoiceGroup(provider.keys.first(), provider.choices, provider.aspectFormat))
}
}
is FlagSpeciesFeatureProvider -> {
if (provider.isAspect && provider.keys.isNotEmpty()) {
flagKeys.add(provider.keys.first())
}
}
}
}
toggles.removeAll(flagKeys)
val filteredChoiceGroups = choiceGroups
.filterNot { it.key in setOf("cosmetic", "cosmetic_item", "cosmeticitem") }
.sortedBy { it.key }
return AspectOptions(
toggles = toggles.filter { it.isNotBlank() && it != "cosmetic" && it != "cosmetic_item" && it != "cosmeticitem" }.toSet(),
choiceGroups = filteredChoiceGroups,
flagKeys = flagKeys.filter { it.isNotBlank() }.toSet(),
cosmeticChoices = collectCosmeticChoices(species, form, gender, shiny)
)
}
fun collectCosmeticChoices(
species: Species,
form: FormData?,
gender: Gender?,
shiny: Boolean?
): List<CosmeticChoice> {
val pokemon = Pokemon().also {
it.isClient = true
it.species = species
it.form = form ?: species.standardForm
if (gender != null) it.gender = gender
if (shiny != null) it.shiny = shiny
}
val registry = BuiltInRegistries.ITEM
val choices = linkedMapOf<ResourceLocation, CosmeticChoice>()
CobblemonCosmeticItems.findValidForPokemon(pokemon).forEach { assignment ->
assignment.cosmeticItems.forEach { cosmetic ->
val aspects = cosmetic.aspects.filter { it.isNotBlank() }
when (val condition = cosmetic.consumedItem) {
is RegistryLikeIdentifierCondition<*> -> {
val id = condition.identifier
val label = registry.getOptional(id)
.map { it.defaultInstance.hoverName.copy() }
.orElseGet { Component.literal(id.toString()) }
choices.putIfAbsent(id, CosmeticChoice(id, aspects, label))
}
is RegistryLikeTagCondition<*> -> {
@Suppress("UNCHECKED_CAST")
val tag = condition.tag as? TagKey<net.minecraft.world.item.Item> ?: return@forEach
val tagItems = registry.getTag(tag).orElse(null) ?: return@forEach
tagItems.forEach { holder ->
val item = holder.value()
val id = registry.getKey(item) ?: return@forEach
val label = item.defaultInstance.hoverName.copy()
choices.putIfAbsent(id, CosmeticChoice(id, aspects, label))
}
}
}
}
}
return choices.values.sortedBy { it.label.string.lowercase() }
}
fun choiceAspectId(key: String, choice: String): String? {
val provider = SpeciesFeatures.getFeature(key) as? ChoiceSpeciesFeatureProvider ?: return null
return provider.aspectFormat.substitute("choice", choice)
}
}
private sealed class AspectItem {
data class Group(val group: ChoiceGroup) : AspectItem()
data class Toggle(val id: String, val source: ToggleSource) : AspectItem()
data class Cosmetic(val choices: List<CosmeticChoice>) : AspectItem()
}
private sealed class OptionItem {
data class Choice(val group: ChoiceGroup, val choice: String) : OptionItem()
data class ClearChoice(val group: ChoiceGroup) : OptionItem()
data class Toggle(val aspect: AspectItem.Toggle, val enabled: Boolean) : OptionItem()
data class CosmeticChoice(val choice: PokeEditAspectsTab.Companion.CosmeticChoice) : OptionItem()
object ClearCosmetic : OptionItem()
}
private enum class ToggleSource {
FORCED_ASPECT,
FLAG_FEATURE
}
private val listX = x + 4
private val listBottom = y + HEIGHT - BOTTOM_MARGIN
private val listAreaWidth = WIDTH - 8
private val listTop = y + TOP_MARGIN
private val listHeight = listBottom - listTop
private val leftWidth = (listAreaWidth - COLUMN_GAP) / 2
private val rightWidth = listAreaWidth - leftWidth - COLUMN_GAP
private val leftX = listX
private val rightX = listX + leftWidth + COLUMN_GAP
private val aspectList = AspectList(
leftX,
listTop,
leftWidth,
listHeight
).also { addWidget(it) }
private val optionList = OptionList(
rightX,
listTop,
rightWidth,
listHeight
).also { addWidget(it) }
private var aspectOptions: AspectOptions = collectAspectOptions(
draft.species,
draft.form,
draft.gender,
draft.shiny
)
private var selectedAspect: AspectItem? = null
private var lastSpeciesId: ResourceLocation? = null
init {
rebuildAspectEntries()
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val currentSpeciesId = draft.species.resourceIdentifier
if (lastSpeciesId != currentSpeciesId) {
refreshAspects()
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.aspects").bold(),
x = x + (WIDTH / 2.0),
y = y + 2,
scale = SCALE,
centered = true
)
context.fill(
leftX,
listTop,
leftX + leftWidth,
listBottom,
0x22000000
)
context.fill(
rightX,
listTop,
rightX + rightWidth,
listBottom,
0x22000000
)
aspectList.render(context, mouseX, mouseY, delta)
optionList.render(context, mouseX, mouseY, delta)
}
private fun refreshAspects() {
aspectOptions = collectAspectOptions(
draft.species,
draft.form,
draft.gender,
draft.shiny
)
val cosmetic = aspectOptions.cosmeticChoices.firstOrNull { it.itemId.toString() == draft.cosmeticItem }
draft.cosmeticItem = cosmetic?.itemId?.toString()
draft.cosmeticAspects = cosmetic?.aspects?.toSet() ?: emptySet()
val valid = buildAspectEntries()
if (selectedAspect !in valid) {
selectedAspect = valid.firstOrNull()
}
rebuildAspectEntries()
lastSpeciesId = draft.species.resourceIdentifier
}
private fun buildAspectEntries(): List<AspectItem> {
val entries = mutableListOf<AspectItem>()
aspectOptions.choiceGroups.sortedBy { it.key }.forEach { entries.add(AspectItem.Group(it)) }
aspectOptions.flagKeys.sortedBy { it }.forEach { entries.add(AspectItem.Toggle(it, ToggleSource.FLAG_FEATURE)) }
aspectOptions.toggles.sortedBy { it }.forEach { entries.add(AspectItem.Toggle(it, ToggleSource.FORCED_ASPECT)) }
return entries
}
private fun rebuildAspectEntries() {
val entries = buildAspectEntries()
aspectList.setEntries(entries)
if (selectedAspect == null) {
selectedAspect = entries.firstOrNull()
}
rebuildOptionEntries()
}
private fun rebuildOptionEntries() {
optionList.setEntries(buildOptionEntries(selectedAspect))
}
private fun buildOptionEntries(aspect: AspectItem?): List<OptionItem> {
if (aspect == null) return emptyList()
return when (aspect) {
is AspectItem.Group -> {
val entries = mutableListOf<OptionItem>()
entries.add(OptionItem.ClearChoice(aspect.group))
aspect.group.choices.forEach { choice ->
entries.add(OptionItem.Choice(aspect.group, choice))
}
entries
}
is AspectItem.Cosmetic -> {
val entries = mutableListOf<OptionItem>()
entries.add(OptionItem.ClearCosmetic)
aspect.choices.forEach { choice ->
entries.add(OptionItem.CosmeticChoice(choice))
}
entries
}
is AspectItem.Toggle -> {
listOf(
OptionItem.Toggle(aspect, enabled = true),
OptionItem.Toggle(aspect, enabled = false)
)
}
}
}
private fun humanizeId(raw: String): String {
return raw
.replace('_', ' ')
.replace('-', ' ')
.split(" ")
.filter { it.isNotBlank() }
.joinToString(" ") { part ->
part.lowercase().replaceFirstChar { ch -> if (ch.isLowerCase()) ch.titlecase() else ch.toString() }
}
}
private fun labelForId(id: String): MutableComponent {
val key = "cobblemon.ui.aspect.$id"
return if (I18n.exists(key)) {
Component.translatable(key)
} else {
Component.literal(humanizeId(id))
}
}
private fun isAspectActive(entry: AspectItem): Boolean = when (entry) {
is AspectItem.Toggle -> when (entry.source) {
ToggleSource.FORCED_ASPECT -> entry.id in draft.forcedAspects
ToggleSource.FLAG_FEATURE -> entry.id in draft.flagFeatures
}
is AspectItem.Group -> entry.group.key in draft.choiceFeatures
is AspectItem.Cosmetic -> draft.cosmeticItem != null
}
private fun labelFor(entry: AspectItem): MutableComponent = when (entry) {
is AspectItem.Group -> {
val name = labelForId(entry.group.key).string
val choice = draft.choiceFeatures[entry.group.key]?.let { labelForId(it).string }
Component.literal(if (choice != null) "$name: $choice" else name)
}
is AspectItem.Toggle -> labelForId(entry.id)
is AspectItem.Cosmetic -> {
val base = lang("cosmetic_item").string
val choice = draft.cosmeticItem?.let { id ->
entry.choices.firstOrNull { it.itemId.toString() == id }?.label?.string
}
Component.literal(if (choice != null) "$base: $choice" else base)
}
}
private fun labelFor(option: OptionItem): MutableComponent = when (option) {
is OptionItem.ClearChoice -> lang("ui.none")
is OptionItem.Choice -> labelForId(option.choice)
is OptionItem.Toggle -> if (option.enabled) lang("ui.enabled") else lang("ui.disabled")
is OptionItem.CosmeticChoice -> option.choice.label
OptionItem.ClearCosmetic -> lang("ui.none")
}
private fun selectAspect(entry: AspectItem) {
if (selectedAspect == entry) return
selectedAspect = entry
rebuildOptionEntries()
}
private fun handleOption(option: OptionItem) {
when (option) {
is OptionItem.ClearChoice -> {
draft.choiceFeatures.remove(option.group.key)
}
is OptionItem.Choice -> {
draft.choiceFeatures[option.group.key] = option.choice
}
is OptionItem.Toggle -> {
when (option.aspect.source) {
ToggleSource.FORCED_ASPECT -> {
if (option.enabled) {
draft.forcedAspects.add(option.aspect.id)
} else {
draft.forcedAspects.remove(option.aspect.id)
}
}
ToggleSource.FLAG_FEATURE -> {
if (option.enabled) {
draft.flagFeatures.add(option.aspect.id)
} else {
draft.flagFeatures.remove(option.aspect.id)
}
}
}
}
is OptionItem.CosmeticChoice -> {
draft.cosmeticItem = option.choice.itemId.toString()
draft.cosmeticAspects = option.choice.aspects.toSet()
}
OptionItem.ClearCosmetic -> {
draft.cosmeticItem = null
draft.cosmeticAspects = emptySet()
}
}
}
private inner class AspectList(
pX: Int,
pY: Int,
width: Int,
height: Int
) : ScrollingWidget<AspectList.AspectEntry>(
width = width,
height = height,
left = pX,
top = pY - height,
slotHeight = SLOT_HEIGHT + 2,
scrollBarWidth = 3
) {
private val listX = pX
fun setEntries(aspects: List<AspectItem>) {
clearEntries()
aspects.forEach { aspect -> addEntry(AspectEntry(aspect)) }
scrollAmount = 0.0
}
override fun getScrollbarPosition(): Int = listX + 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 + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
}
inner class AspectEntry(
private val entry: AspectItem
) : Slot<AspectEntry>() {
private var x: Int = 0
private var y: Int = 0
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
this.x = x
this.y = y + 1
if (selectedAspect == entry) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
} else if (isAspectActive(entry)) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x4436B33F)
} else if (hovered) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = labelFor(entry),
x = this.x + 4,
y = this.y + 3,
scale = SCALE,
shadow = true
)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isMouseOver(mouseX, mouseY)) {
selectAspect(entry)
return true
}
return false
}
override fun getNarration(): Component {
return labelFor(entry)
}
}
}
private inner class OptionList(
pX: Int,
pY: Int,
width: Int,
height: Int
) : ScrollingWidget<OptionList.OptionEntry>(
width = width,
height = height,
left = pX,
top = pY - height,
slotHeight = SLOT_HEIGHT + 2,
scrollBarWidth = 3
) {
private val listX = pX
fun setEntries(options: List<OptionItem>) {
clearEntries()
options.forEach { option -> addEntry(OptionEntry(option)) }
scrollAmount = 0.0
}
override fun getScrollbarPosition(): Int = listX + 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 + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
}
inner class OptionEntry(
private val option: OptionItem
) : Slot<OptionEntry>() {
private var x: Int = 0
private var y: Int = 0
private fun isSelectedOption(): Boolean = when (option) {
is OptionItem.ClearChoice -> {
draft.choiceFeatures[option.group.key] == null
}
is OptionItem.Choice -> {
draft.choiceFeatures[option.group.key] == option.choice
}
is OptionItem.Toggle -> {
val isEnabled = when (option.aspect.source) {
ToggleSource.FORCED_ASPECT -> option.aspect.id in draft.forcedAspects
ToggleSource.FLAG_FEATURE -> option.aspect.id in draft.flagFeatures
}
isEnabled == option.enabled
}
is OptionItem.CosmeticChoice -> {
draft.cosmeticItem == option.choice.itemId.toString()
}
OptionItem.ClearCosmetic -> {
draft.cosmeticItem == null
}
}
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
this.x = x
this.y = y + 1
if (isSelectedOption()) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x4436B33F)
} else if (hovered) {
context.fill(this.x - 1, this.y - 1, this.x + entryWidth + 1, this.y + SLOT_HEIGHT + 1, 0x442A7FFF)
}
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = labelFor(option),
x = this.x + 4,
y = this.y + 3,
scale = SCALE,
shadow = true
)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isMouseOver(mouseX, mouseY)) {
handleOption(option)
return true
}
return false
}
override fun getNarration(): Component {
return labelFor(option)
}
}
}
}
@@ -1,335 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.pokemon.EVs
import com.cobblemon.mod.common.pokemon.IVs
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.BufferUploader
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.Tesselator
import com.mojang.blaze3d.vertex.VertexFormat
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.MutableComponent
import net.minecraft.world.phys.Vec2
import org.joml.Vector3f
class PokeEditEVIVTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.pokeedit.stats")) {
companion object {
private const val WIDTH = 139
private const val HEIGHT = 163
private const val CHART_WIDTH = 134
private const val CHART_HEIGHT = 148
private const val SCALE = 0.5F
private const val TAB_MARKER_WIDTH = 8
private const val TAB_IV = 0
private const val TAB_EV = 1
private val statsBaseResource = cobblemonResource("textures/gui/summary/summary_stats_chart_base.png")
private val statsChartResource = cobblemonResource("textures/gui/summary/summary_stats_chart.png")
private val tabMarkerResource = cobblemonResource("textures/gui/summary/summary_stats_tab_marker.png")
private val statLabels = listOf(
lang("ui.stats.hp").bold() to Stats.HP,
lang("ui.stats.atk").bold() to Stats.ATTACK,
lang("ui.stats.def").bold() to Stats.DEFENCE,
lang("ui.stats.speed").bold() to Stats.SPEED,
lang("ui.stats.sp_def").bold() to Stats.SPECIAL_DEFENCE,
lang("ui.stats.sp_atk").bold() to Stats.SPECIAL_ATTACK
)
private val hexagonVerticesOffset = listOf(
Pair(67.0, 10.5), // 12 o'clock
Pair(122.0, 42.5), // 2 o'clock
Pair(122.0, 93.5), // 4 o'clock
Pair(67.0, 124.5), // 6 o'clock
Pair(12.0, 93.5), // 8 o'clock
Pair(12.0, 42.5) // 10 o'clock
)
}
private var tabIndex = TAB_IV
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val chartX = x + 2
val chartY = y + 6
blitk(
matrixStack = context.pose(),
texture = statsBaseResource,
x = chartX,
y = chartY,
width = CHART_WIDTH,
height = CHART_HEIGHT
)
blitk(
matrixStack = context.pose(),
texture = statsChartResource,
x = (chartX + 25.5) / SCALE,
y = (chartY + 22) / SCALE,
width = 166,
height = 192,
scale = SCALE
)
if (tabIndex == TAB_IV) {
drawStatPolygon(
chartX.toFloat(),
chartY.toFloat(),
statLabels.map { (_, stat) -> draft.ivs.getOrDefault(stat).toFloat() / IVs.MAX_VALUE.toFloat() },
colour = Vector3f(216F / 255F, 100F / 255F, 1F)
)
} else {
drawStatPolygon(
chartX.toFloat(),
chartY.toFloat(),
statLabels.map { (_, stat) -> draft.evs.getOrDefault(stat).toFloat() / EVs.MAX_STAT_VALUE.toFloat() },
colour = Vector3f(1F, 1F, 100F / 255F)
)
}
val labelsHovered = statLabelsHovered(chartX, chartY, mouseX, mouseY)
val labels = statLabels.map { (label, stat) ->
if (labelsHovered) {
if (tabIndex == TAB_IV) {
"${draft.ivs.getOrDefault(stat)}/${IVs.MAX_VALUE}".text()
} else {
"${draft.evs.getOrDefault(stat)}/${EVs.MAX_STAT_VALUE}".text()
}
} else {
label
}
}
renderPolygonLabels(
context = context,
chartX = chartX.toDouble(),
chartY = chartY.toDouble(),
labels = labels,
verticesOffset = hexagonVerticesOffset
)
val values = statLabels.map { (_, stat) ->
if (tabIndex == TAB_IV) {
draft.ivs.getOrDefault(stat).toString().text()
} else {
draft.evs.getOrDefault(stat).toString().text()
}
}
renderPolygonLabels(
context = context,
chartX = chartX.toDouble(),
chartY = chartY.toDouble(),
labels = values,
verticesOffset = hexagonVerticesOffset,
offsetY = 5.5
)
if (tabIndex == TAB_EV) {
drawScaledText(
context = context,
text = lang("ui.pokeedit.evs.total", draft.evs.total(), EVs.MAX_TOTAL_VALUE),
x = chartX + 10,
y = chartY + 126,
scale = SCALE,
shadow = true
)
}
renderTabs(context, chartX, chartY)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val tabIndexAtPos = getTabIndexFromPos(mouseX, mouseY)
if (tabIndexAtPos != -1 && tabIndexAtPos != tabIndex) {
tabIndex = tabIndexAtPos
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
val chartX = x + 2
val chartY = y + 6
val stat = getHoveredStat(chartX, chartY, mouseX.toInt(), mouseY.toInt()) ?: return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
val step = when {
Screen.hasControlDown() -> 25
Screen.hasShiftDown() -> 10
else -> 1
}
val delta = if (verticalAmount > 0) step else -step
if (tabIndex == TAB_IV) {
val current = draft.ivs.getOrDefault(stat)
val next = (current + delta).coerceIn(0, IVs.MAX_VALUE)
draft.ivs[stat] = next
} else {
val current = draft.evs.getOrDefault(stat)
val totalWithout = draft.evs.total() - current
val maxForStat = minOf(EVs.MAX_STAT_VALUE, EVs.MAX_TOTAL_VALUE - totalWithout)
val next = (current + delta).coerceIn(0, maxForStat)
draft.evs[stat] = next
}
return true
}
private fun renderTabs(context: GuiGraphics, chartX: Int, chartY: Int) {
val tabWidth = 40
val tabLeft = chartX + ((CHART_WIDTH - (tabWidth * 2)) / 2)
val tabCenters = listOf(
tabLeft + (tabWidth / 2),
tabLeft + tabWidth + (tabWidth / 2)
)
val tabY = chartY + 137
drawScaledText(
context = context,
text = lang("ui.stats.ivs").bold(),
x = tabCenters[0],
y = tabY,
scale = SCALE,
colour = if (tabIndex == TAB_IV) 0x00FFFFFF else 0x008F8F8F,
centered = true
)
drawScaledText(
context = context,
text = lang("ui.stats.evs").bold(),
x = tabCenters[1],
y = tabY,
scale = SCALE,
colour = if (tabIndex == TAB_EV) 0x00FFFFFF else 0x008F8F8F,
centered = true
)
blitk(
matrixStack = context.pose(),
texture = tabMarkerResource,
x = (tabCenters[tabIndex] - (TAB_MARKER_WIDTH / 2)) / SCALE,
y = (tabY - 3) / SCALE,
width = TAB_MARKER_WIDTH,
height = 4,
scale = SCALE
)
}
private fun getTabIndexFromPos(mouseX: Double, mouseY: Double): Int {
val chartX = x + 2
val chartY = y + 6
val tabWidth = 40
val tabLeft = chartX + ((CHART_WIDTH - (tabWidth * 2)) / 2)
val tabTop = chartY + 134.0
val tabBottom = tabTop + 9.0
if (mouseY !in tabTop..tabBottom) return -1
if (mouseX in tabLeft.toDouble()..(tabLeft + tabWidth).toDouble()) return TAB_IV
if (mouseX in (tabLeft + tabWidth).toDouble()..(tabLeft + tabWidth * 2).toDouble()) return TAB_EV
return -1
}
private fun getHoveredStat(chartX: Int, chartY: Int, mouseX: Int, mouseY: Int): Stat? {
statLabels.forEachIndexed { index, (_, stat) ->
val pos = hexagonVerticesOffset[index]
if (mouseX.toDouble() in (chartX + pos.first - 12)..(chartX + pos.first + 12)
&& mouseY.toDouble() in (chartY + pos.second - 1)..(chartY + pos.second + 10)
) {
return stat
}
}
return null
}
private fun statLabelsHovered(chartX: Int, chartY: Int, mouseX: Int, mouseY: Int): Boolean {
return getHoveredStat(chartX, chartY, mouseX, mouseY) != null
}
private fun renderPolygonLabels(
context: GuiGraphics,
chartX: Double,
chartY: Double,
labels: List<MutableComponent>,
verticesOffset: List<Pair<Double, Double>>,
offsetY: Double = 0.0
) {
if (labels.size != verticesOffset.size) return
labels.forEachIndexed { index, label ->
drawScaledText(
context = context,
text = label,
x = chartX + verticesOffset[index].first,
y = chartY + verticesOffset[index].second + offsetY,
scale = SCALE,
centered = true
)
}
}
private fun drawTriangle(colour: Vector3f, v1: Vec2, v2: Vec2, v3: Vec2, opacity: Float = 0.6F) {
RenderSystem.setShaderTexture(0, com.cobblemon.mod.common.client.CobblemonResources.WHITE)
RenderSystem.setShaderColor(colour.x, colour.y, colour.z, opacity)
val bufferBuilder = Tesselator.getInstance().begin(VertexFormat.Mode.TRIANGLES, DefaultVertexFormat.POSITION)
bufferBuilder.addVertex(v1.x, v1.y, 10F)
bufferBuilder.addVertex(v2.x, v2.y, 10F)
bufferBuilder.addVertex(v3.x, v3.y, 10F)
BufferUploader.drawWithShader(bufferBuilder.buildOrThrow())
RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
}
private fun drawStatPolygon(chartX: Float, chartY: Float, ratios: List<Float>, colour: Vector3f) {
if (ratios.size != 6) return
val topVertexY = chartY + 22F
val centerX = chartX + 67F
val centerY = topVertexY + 48F
val radius = centerY - topVertexY
val startAngleDeg = -90.0
val angleStepDeg = 360.0 / 6
val anglesDeg = List(6) { i -> startAngleDeg + i * angleStepDeg }
val anglesRad = anglesDeg.map { Math.toRadians(it) }
val maxPoints = anglesRad.map { angle ->
Vec2(
centerX + radius * Math.cos(angle).toFloat(),
centerY + radius * Math.sin(angle).toFloat()
)
}
val coercedRatios = ratios.map { it.coerceIn(5 / radius, 1F) }
val vertices = maxPoints.mapIndexed { index, maxPoint ->
Vec2(
centerX + (maxPoint.x - centerX) * coercedRatios[index],
centerY + (maxPoint.y - centerY) * coercedRatios[index]
)
}
val centerPoint = Vec2(centerX, centerY)
RenderSystem.disableDepthTest()
for (i in vertices.indices) {
val nextIndex = (i + 1) % vertices.size
drawTriangle(colour, vertices[i], centerPoint, vertices[nextIndex])
}
RenderSystem.enableDepthTest()
}
}
@@ -1,354 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
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.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.mojang.blaze3d.platform.Lighting
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Blocks
import org.joml.Quaternionf
import org.joml.Vector3f
class PokeEditImplementationBlankTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft,
title: Component
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, title) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
private const val CUBE_BASE_OFFSET_X = 1.0F
private const val CUBE_SIZE_OFFSET_MULTIPLIER = 0.18F
private const val CUBE_OFFSET_Z = -0.5F
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
DoubleValue(hasNonWearableAttachment())
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(hasWearableHatAttachment())
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(hasWearableFaceAttachment())
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var attachmentStacks: List<ItemStack> = emptyList()
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE)
private var selectedPoseIndex: Int = 0
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = context.pose(),
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = context.pose(),
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
buildRenderable()?.let { renderable ->
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
val matrices = context.pose()
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
val rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, rotationY, 0F))
val cubeOffsetX = getCubeOffsetX()
matrices.translate((-cubeOffsetX / 2F).toDouble(), 0.0, 0.0)
renderReferenceCube(renderable, matrices, rotation, cubeOffsetX)
drawProfilePokemon(
renderablePokemon = renderable,
poseType = poseList[selectedPoseIndex],
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf(rotation),
state = state,
scale = MODEL_DRAW_SCALE
)
if (attachmentStacks.isNotEmpty()) {
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
attachmentStacks.forEach { stack ->
heldItemRenderer.renderOnModel(
stack,
state,
matrices,
bufferSource,
light = 0xF000F0,
frontLight = true
)
}
bufferSource.endBatch()
}
matrices.popPose()
context.disableScissor()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = parseItemStack(draft.heldItem)
val cosmeticStack = parseItemStack(draft.cosmeticItem)
attachmentStacks = buildAttachmentStacks(heldStack, cosmeticStack)
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun parseItemStack(rawId: String?): ItemStack {
return rawId
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
}
private fun buildAttachmentStacks(heldStack: ItemStack, cosmeticStack: ItemStack): List<ItemStack> {
if (heldStack.isEmpty && cosmeticStack.isEmpty) return emptyList()
val stacks = mutableListOf<ItemStack>()
if (!heldStack.isEmpty) {
stacks.add(heldStack)
}
if (!cosmeticStack.isEmpty && (heldStack.isEmpty || cosmeticStack.item != heldStack.item)) {
stacks.add(cosmeticStack)
}
return stacks
}
private fun hasNonWearableAttachment(): Boolean {
return attachmentStacks.any { stack ->
!stack.isEmpty && !stack.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !stack.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
}
}
private fun hasWearableHatAttachment(): Boolean {
return attachmentStacks.any { stack -> !stack.isEmpty && stack.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) }
}
private fun hasWearableFaceAttachment(): Boolean {
return attachmentStacks.any { stack -> !stack.isEmpty && stack.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS) }
}
private fun renderReferenceCube(
renderable: RenderablePokemon,
matrices: com.mojang.blaze3d.vertex.PoseStack,
rotation: Quaternionf,
cubeOffsetX: Float
) {
val model = VaryingModelRepository.getPoser(renderable.species.resourceIdentifier, state)
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
matrices.pushPose()
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
matrices.translate(
model.profileTranslation.x,
model.profileTranslation.y + 1.5 * model.profileScale,
model.profileTranslation.z - 4.0
)
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
matrices.mulPose(rotation)
matrices.mulPose(com.mojang.math.Axis.XP.rotationDegrees(180F))
matrices.translate(cubeOffsetX.toDouble(), 0.0, CUBE_OFFSET_Z.toDouble())
RenderSystem.applyModelViewMatrix()
RenderSystem.enableDepthTest()
RenderSystem.disableCull()
Lighting.setupForEntityInInventory()
Minecraft.getInstance().blockRenderer.renderSingleBlock(
Blocks.RED_WOOL.defaultBlockState(),
matrices,
bufferSource,
LightTexture.pack(15, 15),
OverlayTexture.NO_OVERLAY
)
bufferSource.endBatch()
Lighting.setupFor3DItems()
RenderSystem.enableCull()
matrices.popPose()
}
private fun getCubeOffsetX(): Float {
val hitboxWidth = draft.form.hitbox.width * draft.form.baseScale
val hitboxHeight = draft.form.hitbox.height * draft.form.baseScale
val hitboxSize = maxOf(hitboxWidth, hitboxHeight)
val extraOffset = (hitboxSize - 1.0F).coerceAtLeast(0.0F) * CUBE_SIZE_OFFSET_MULTIPLIER
return CUBE_BASE_OFFSET_X + extraOffset
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -1,227 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
class PokeEditLocatorListWidget(
val pX: Int,
val pY: Int,
val listWidth: Int,
val listHeight: Int,
private val onSelectLocator: (String?) -> Unit
) : ScrollingWidget<PokeEditLocatorListWidget.LocatorRow>(
left = pX,
top = pY - listHeight,
width = listWidth,
height = listHeight,
slotHeight = ROW_HEIGHT
) {
companion object {
private const val ROW_HEIGHT = 9
private const val TEXT_SCALE = 0.5F
private const val BORDER_COLOR = 0xFF1B1B1B.toInt()
private const val FILL_COLOR = 0xFF3B3B3B.toInt()
private const val TEXT_COLOR = 0xFFFCFCFC.toInt()
}
private val listTop = pY - listHeight
private val listBottom = pY
private var selectedLocator: String? = null
private var draggingScrollbar = false
fun setLocators(locators: Collection<String>) {
clearEntries()
locators.sorted().forEach { addEntry(LocatorRow(it)) }
if (selectedLocator != null && locators.none { it == selectedLocator }) {
selectedLocator = null
onSelectLocator(null)
}
scrollAmount = 0.0
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
context.fill(pX - 1, listTop - 1, pX + listWidth + 1, listBottom + 1, BORDER_COLOR)
context.fill(pX, listTop, pX + listWidth, listBottom, FILL_COLOR)
focused = if (mouseX in pX..(pX + listWidth) && mouseY in listTop..listBottom) {
getEntryAtPosition(mouseX.toDouble(), mouseY.toDouble())
} else {
null
}
context.enableScissor(pX, listTop, pX + listWidth, listBottom)
renderListItems(context, mouseX, mouseY, delta)
context.disableScissor()
if (maxScroll > 0) {
renderScrollbar(context, mouseX, mouseY, delta)
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
) {
return false
}
val maxScroll = (itemCount * itemHeight - listHeight).coerceAtLeast(0)
if (maxScroll <= 0) return true
val step = if (verticalAmount != 0.0) verticalAmount else amount
scrollAmount = Mth.clamp(scrollAmount - step * itemHeight, 0.0, maxScroll.toDouble())
return true
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (mouseX !in pX.toDouble()..(pX + listWidth).toDouble()
|| mouseY !in listTop.toDouble()..listBottom.toDouble()
) {
return false
}
if (button != 0) {
return false
}
if (isOverScrollbar(mouseX, mouseY) && maxScroll > 0) {
draggingScrollbar = true
return true
}
val localY = mouseY.toInt() - listTop + scrollAmount.toInt()
if (localY < 0) return false
val index = localY / itemHeight
if (index !in 0 until itemCount) return false
val entry = getEntry(index)
if (entry is LocatorRow) {
selectedLocator = entry.name
onSelectLocator(selectedLocator)
return true
}
return false
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (button == 0 && draggingScrollbar) {
draggingScrollbar = false
return true
}
return super.mouseReleased(mouseX, mouseY, button)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (!draggingScrollbar || maxScroll <= 0) {
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
val barHeight = listBottom - listTop
var thumbHeight = ((barHeight * barHeight).toFloat() / maxPosition.toFloat()).toInt()
thumbHeight = Mth.clamp(thumbHeight, 16, barHeight - 4)
val trackHeight = barHeight - thumbHeight
if (trackHeight <= 0) return true
val clampedY = Mth.clamp(mouseY.toInt() - listTop - thumbHeight / 2, 0, trackHeight)
scrollAmount = (clampedY.toDouble() * maxScroll.toDouble()) / trackHeight.toDouble()
return true
}
override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
return mouseX in pX.toDouble()..(pX + listWidth).toDouble()
&& mouseY in listTop.toDouble()..listBottom.toDouble()
}
override fun renderListItems(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val rowX = rowLeft
for (index in 0 until itemCount) {
val rowY = getRowTop(index)
val rowBottom = getRowBottom(index)
if (rowBottom >= listTop && rowY <= listBottom) {
val entry = getEntry(index)
if (entry is LocatorRow) {
entry.isSelected = entry.name == selectedLocator
}
renderItem(context, mouseX, mouseY, delta, index, rowX, rowY, rowWidth, itemHeight)
}
}
}
override fun renderScrollbar(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val xLeft = this.scrollbarPosition
val xRight = xLeft + 3
val yStart = listTop
val barHeight = listBottom - listTop
var yBottom = ((barHeight * barHeight).toFloat() / this.maxPosition.toFloat()).toInt()
yBottom = Mth.clamp(yBottom, 16, barHeight - 4)
var yTop = scrollAmount.toInt() * (barHeight - yBottom) / this.maxScroll + yStart
if (yTop < yStart) yTop = yStart
context.fill(xLeft + 1, yStart, xRight - 1, listBottom, 0xFF7AEEF1.toInt())
context.fill(xLeft, yTop, xRight, yTop + yBottom, 0xFF3A96B6.toInt())
}
override fun getRowTop(index: Int): Int {
return listTop - scrollAmount.toInt() + (index * itemHeight)
}
override fun getRowBottom(index: Int): Int {
return getRowTop(index) + itemHeight
}
override fun getBottom(): Int {
return listBottom
}
private fun isOverScrollbar(mouseX: Double, mouseY: Double): Boolean {
val xLeft = scrollbarPosition
val xRight = xLeft + 3
return mouseX >= xLeft && mouseX <= xRight && mouseY >= listTop && mouseY <= listBottom
}
class LocatorRow(val name: String) : Slot<LocatorRow>() {
var isSelected: Boolean = false
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
val bg = when {
isSelected -> 0xFF4A4A4A.toInt()
hovered -> 0xFF2F2F2F.toInt()
else -> null
}
if (bg != null) {
context.fill(x + 1, y, x + entryWidth - 1, y + ROW_HEIGHT, bg)
}
drawScaledText(
context = context,
text = name.text(),
x = x + 3,
y = y + 2,
scale = TEXT_SCALE,
colour = TEXT_COLOR,
shadow = false
)
}
override fun getNarration(): Component {
return Component.literal(name)
}
}
}
@@ -1,476 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
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.summary.SummaryButton
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.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.renderer.LevelRenderer
import net.minecraft.client.renderer.RenderType
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import org.joml.Matrix4f
import org.joml.Quaternionf
import org.joml.Vector4f
import org.joml.Vector3f
class PokeEditLocatorTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft,
private var highlightedLocator: String?,
private val onDumpLocators: (List<String>) -> Unit
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, "locators".text()) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
private const val LOCATOR_CUBE_SIZE = 0.1
private const val LOCATOR_TOOLTIP_RADIUS = 8
private const val BUTTON_WIDTH = 54
private const val BUTTON_HEIGHT = 15
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
private const val AXIS_LINE_LENGTH = 0.35
private const val LEGEND_SCALE = 0.5F
private const val LEGEND_X = 6
private const val LEGEND_Y = 60
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
val held = currentHeldStack()
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.PROFILE)
private var selectedPoseIndex: Int = 0
private var showLocators = false
private var showLocatorAxes = false
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
private var cachedLocatorNames: List<String> = emptyList()
private val showLocatorsButton = SummaryButton(
buttonX = pX + 6F,
buttonY = pY + 6F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocators = true },
text = "Locators On".text(),
resource = actionButtonResource,
renderRequirement = { !showLocators },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
private val hideLocatorsButton = SummaryButton(
buttonX = pX + 66F,
buttonY = pY + 6F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocators = false },
text = "Locators Off".text(),
resource = actionButtonResource,
renderRequirement = { showLocators },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
private val showLocatorAxesButton = SummaryButton(
buttonX = pX + 6F,
buttonY = pY + 24F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocatorAxes = true },
text = "Axes On".text(),
resource = actionButtonResource,
renderRequirement = { !showLocatorAxes },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
private val hideLocatorAxesButton = SummaryButton(
buttonX = pX + 66F,
buttonY = pY + 24F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { showLocatorAxes = false },
text = "Axes Off".text(),
resource = actionButtonResource,
renderRequirement = { showLocatorAxes },
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
showLocatorsButton.buttonActive = showLocators
hideLocatorsButton.buttonActive = !showLocators
showLocatorAxesButton.buttonActive = showLocatorAxes
hideLocatorAxesButton.buttonActive = !showLocatorAxes
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = matrices,
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
buildRenderable()?.let { renderable ->
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
val rotationVector = Vector3f(13F, rotationY, 0F)
drawProfilePokemon(
renderablePokemon = renderable,
poseType = poseList[selectedPoseIndex],
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
state = state,
scale = MODEL_DRAW_SCALE
)
refreshLocatorList()
if (!renderable.heldItem.isEmpty) {
heldItemRenderer.renderOnModel(
renderable.heldItem,
state,
matrices,
context.bufferSource(),
light = 0xF000F0,
frontLight = true
)
}
if (showLocators) {
renderLocators(context, mouseX, mouseY, showLocatorAxes)
}
matrices.popPose()
context.disableScissor()
}
showLocatorsButton.render(context, mouseX, mouseY, delta)
hideLocatorsButton.render(context, mouseX, mouseY, delta)
showLocatorAxesButton.render(context, mouseX, mouseY, delta)
hideLocatorAxesButton.render(context, mouseX, mouseY, delta)
if (showLocators && showLocatorAxes) {
renderAxisLegend(context)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun renderLocators(context: GuiGraphics, mouseX: Int, mouseY: Int, showAxes: Boolean) {
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
val buffer = bufferSource.getBuffer(RenderType.lines())
val baseMatrix = Matrix4f(context.pose().last().pose())
var closestName: String? = null
var closestDistance = LOCATOR_TOOLTIP_RADIUS.toFloat()
for ((name, matrix) in state.locatorStates.entries) {
val highlight = name == highlightedLocator
val origin = matrix.getOrigin()
context.pose().pushPose()
context.pose().mulPose(matrix.matrix)
LevelRenderer.renderLineBox(
context.pose(),
buffer,
AABB.ofSize(Vec3.ZERO, LOCATOR_CUBE_SIZE, LOCATOR_CUBE_SIZE, LOCATOR_CUBE_SIZE),
if (highlight) 1F else 0F,
if (highlight) 0F else 1F,
0F,
1F
)
if (showAxes) {
renderLocatorAxes(context, buffer)
}
context.pose().popPose()
val screenPos = projectToScreen(baseMatrix, origin) ?: continue
val dx = screenPos.first - mouseX
val dy = screenPos.second - mouseY
val dist = kotlin.math.sqrt((dx * dx + dy * dy).toDouble()).toFloat()
if (dist <= closestDistance) {
closestDistance = dist
closestName = name
}
}
bufferSource.endBatch(RenderType.lines())
if (closestName != null) {
context.renderTooltip(Minecraft.getInstance().font, Component.literal(closestName), mouseX, mouseY)
}
}
fun setHighlightedLocator(name: String?) {
highlightedLocator = name
}
private fun refreshLocatorList() {
val names = state.locatorStates.keys.sorted()
if (names != cachedLocatorNames) {
cachedLocatorNames = names
onDumpLocators(names)
}
}
private fun renderLocatorAxes(context: GuiGraphics, buffer: com.mojang.blaze3d.vertex.VertexConsumer) {
val pose = context.pose().last().pose()
val length = AXIS_LINE_LENGTH.toFloat()
addAxisLine(buffer, pose, 0f, 0f, 0f, length, 0f, 0f, 1f, 0f, 0f)
addAxisLine(buffer, pose, 0f, 0f, 0f, 0f, length, 0f, 0f, 1f, 0f)
addAxisLine(buffer, pose, 0f, 0f, 0f, 0f, 0f, length, 0f, 0f, 1f)
}
private fun addAxisLine(
buffer: com.mojang.blaze3d.vertex.VertexConsumer,
pose: Matrix4f,
x1: Float,
y1: Float,
z1: Float,
x2: Float,
y2: Float,
z2: Float,
r: Float,
g: Float,
b: Float
) {
buffer.addVertex(pose, x1, y1, z1).setColor(r, g, b, 1f).setNormal(0f, 1f, 0f)
buffer.addVertex(pose, x2, y2, z2).setColor(r, g, b, 1f).setNormal(0f, 1f, 0f)
}
private fun renderAxisLegend(context: GuiGraphics) {
val legendX = x + LEGEND_X
val legendY = y + LEGEND_Y
context.fill(legendX - 2, legendY - 2, legendX + 46, legendY + 16, 0xAA1B1B1B.toInt())
drawLegendLine(context, legendX, legendY, 0xFFFF4D4D.toInt(), "X")
drawLegendLine(context, legendX, legendY + 5, 0xFF4DFF4D.toInt(), "Y")
drawLegendLine(context, legendX, legendY + 10, 0xFF4D4DFF.toInt(), "Z")
}
private fun drawLegendLine(context: GuiGraphics, x: Int, y: Int, color: Int, label: String) {
context.fill(x, y + 2, x + 6, y + 3, color)
drawScaledText(
context = context,
text = Component.literal(label),
x = x + 8,
y = y,
scale = LEGEND_SCALE,
colour = 0xFFFFFFFF.toInt(),
shadow = false
)
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = draft.heldItem
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun currentHeldStack(): ItemStack {
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
}
private fun projectToScreen(baseMatrix: Matrix4f, position: Vec3): Pair<Float, Float>? {
val projection = RenderSystem.getProjectionMatrix()
val vec = Vector4f(position.x.toFloat(), position.y.toFloat(), position.z.toFloat(), 1f)
vec.mul(baseMatrix)
vec.mul(projection)
if (vec.w == 0f) return null
vec.div(vec.w)
val window = Minecraft.getInstance().window
val screenX = (vec.x * 0.5f + 0.5f) * window.guiScaledWidth.toFloat()
val screenY = (1f - (vec.y * 0.5f + 0.5f)) * window.guiScaledHeight.toFloat()
return Pair(screenX, screenY)
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -1,659 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
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.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.MoveCategoryIcon
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.common.SummaryScrollList
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.MoveDescriptionScrollList
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.ReorderMoveButton
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 java.math.RoundingMode
import java.text.DecimalFormat
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.util.Mth
class PokeEditMovesTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, WIDTH, HEIGHT, lang("ui.moves")) {
companion object {
private const val WIDTH = 139
private const val HEIGHT = 163
private const val BASE_WIDTH = 134
private const val BASE_HEIGHT = 148
private const val BASE_SCALE = 0.80F
private const val SLOT_SPACING = 3
private const val SCALE = 0.5F
private val decimalFormat = DecimalFormat("#.##").also {
it.roundingMode = RoundingMode.CEILING
}
private val movesBaseResource = cobblemonResource("textures/gui/summary/summary_moves_base.png")
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")
private val moveSwapButtonResource = cobblemonResource("textures/gui/summary/summary_move_swap.png")
private val moveAddButtonResource = cobblemonResource("textures/gui/summary/summary_move_add.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 movesEffectIconResource = cobblemonResource("textures/gui/summary/summary_moves_icon_effect.png")
private val moveListResource = cobblemonResource("textures/gui/summary/summary_move_condensed.png")
private val moveListOverlayResource = cobblemonResource("textures/gui/summary/summary_move_overlay_condensed.png")
private const val MOVE_WIDTH = 108
private const val MOVE_HEIGHT = 22
private const val MOVE_LIST_SLOT_HEIGHT = 18
private const val MOVE_LIST_SLOT_SPACING = 3
private const val MOVE_LIST_X_OFFSET = 0
private const val MOVE_LIST_Y_OFFSET = 0
}
private val baseScaledWidth = (BASE_WIDTH * BASE_SCALE).toInt()
private val baseScaledHeight = (BASE_HEIGHT * BASE_SCALE).toInt()
private val moveWidthScaled = (MOVE_WIDTH * BASE_SCALE).toInt()
private val moveHeightScaled = (MOVE_HEIGHT * BASE_SCALE).toInt()
private val slotSpacingScaled = (SLOT_SPACING * BASE_SCALE).toInt().coerceAtLeast(1)
private val baseX = x + (WIDTH - baseScaledWidth) / 2 - 10
private val baseY = y + 7
private val slotX = baseX + (13 * BASE_SCALE).toInt()
private val slotY = baseY + (6 * BASE_SCALE).toInt()
private val descriptionListX = (baseX + (69 * BASE_SCALE).toInt())
.coerceAtMost(baseX + baseScaledWidth - 60)
private val descriptionListY = baseY + (113 * BASE_SCALE).toInt()
private val slots = (0 until 4).map { index ->
MoveSlot(
slotX,
slotY + (moveHeightScaled + slotSpacingScaled) * index,
index
).also { addWidget(it) }
}
private val descriptionList = MoveDescriptionScrollList(
descriptionListX,
descriptionListY,
5
)
private val movePicker = MovePickerList(
baseX + baseScaledWidth + MOVE_LIST_X_OFFSET,
baseY + (11 * BASE_SCALE).toInt() + MOVE_LIST_Y_OFFSET
)
private var selectedSlotIndex = -1
private var pickerSlotIndex = -1
private var movePickerOpen = false
private var lastDescriptionKey: String? = null
init {
refreshMoves()
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
blitk(
matrixStack = matrices,
texture = movesBaseResource,
x = baseX,
y = baseY,
width = baseScaledWidth,
height = baseScaledHeight
)
slots.forEach { it.render(context, mouseX, mouseY, delta) }
renderMoveInfo(context)
renderDescription(context, mouseX, mouseY, delta)
if (movePickerOpen) {
movePicker.renderWidget(context, mouseX, mouseY, delta)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (movePickerOpen) {
val handled = movePicker.mouseClicked(mouseX, mouseY, button)
if (handled) return true
if (!movePicker.isMouseOver(mouseX, mouseY)) {
closeMovePicker()
}
}
if (descriptionList.isHovered) {
descriptionList.mouseClicked(mouseX, mouseY, button)
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
if (movePickerOpen) {
if (movePicker.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) return true
}
if (descriptionList.isHovered) {
descriptionList.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (movePickerOpen) {
if (movePicker.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true
}
if (descriptionList.isHovered) {
descriptionList.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun refreshMoves() {
val moves = Moves.all().sortedBy { it.displayName.string }
movePicker.setEntries(moves)
}
private fun selectedMove(): MoveTemplate? {
return if (selectedSlotIndex in draft.activeMoves.indices) draft.activeMoves[selectedSlotIndex] else null
}
private fun renderMoveInfo(context: GuiGraphics) {
val move = selectedMove()
val mcFont = Minecraft.getInstance().font
// Power
blitk(
matrixStack = context.pose(),
texture = movesPowerIconResource,
x = (baseX + (7 * BASE_SCALE)) / SCALE,
y = (baseY + (114.5 * BASE_SCALE)) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
drawScaledText(
context = context,
text = lang("ui.power"),
x = baseX + (14 * BASE_SCALE),
y = baseY + (115 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
val powerText = if (move != null && move.power.toInt() > 0) move.power.toInt().toString() else "-"
val powerComponent = Component.literal(powerText)
drawScaledText(
context = context,
text = powerComponent,
x = (baseX + (62.5 * BASE_SCALE)) - (mcFont.width(powerComponent) * SCALE * BASE_SCALE),
y = baseY + (115 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
// Accuracy
blitk(
matrixStack = context.pose(),
texture = movesAccuracyIconResource,
x = (baseX + (7 * BASE_SCALE)) / SCALE,
y = (baseY + (125.5 * BASE_SCALE)) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
drawScaledText(
context = context,
text = lang("ui.accuracy"),
x = baseX + (14 * BASE_SCALE),
y = baseY + (126 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
val accuracyText = if (move != null) formatPercent(move.accuracy) else "-"
val accuracyComponent = Component.literal(accuracyText)
drawScaledText(
context = context,
text = accuracyComponent,
x = (baseX + (62.5 * BASE_SCALE)) - (mcFont.width(accuracyComponent) * SCALE * BASE_SCALE),
y = baseY + (126 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
// Category
blitk(
matrixStack = context.pose(),
texture = movesEffectIconResource,
x = (baseX + (7 * BASE_SCALE)) / SCALE,
y = (baseY + (136.5 * BASE_SCALE)) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
drawScaledText(
context = context,
text = lang("ui.category"),
x = baseX + (14 * BASE_SCALE),
y = baseY + (137 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
if (move != null) {
MoveCategoryIcon(
x = baseX + (51 * BASE_SCALE),
y = baseY + (137 * BASE_SCALE),
category = move.damageCategory
).render(context)
drawScaledText(
context = context,
text = move.damageCategory.displayName.copy(),
x = baseX + (61 * BASE_SCALE),
y = baseY + (137 * BASE_SCALE),
scale = SCALE * BASE_SCALE,
shadow = true
)
}
}
private fun renderDescription(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val move = selectedMove()
val nextKey = move?.name ?: ""
if (nextKey != lastDescriptionKey) {
descriptionList.setMoveDescription(move?.description ?: Component.literal(""))
lastDescriptionKey = nextKey
}
if (move != null) {
descriptionList.renderWidget(context, mouseX, mouseY, delta)
}
}
private fun formatPercent(input: Double): String {
if (input <= 0) return "-"
return "${decimalFormat.format(input)}%"
}
private fun toggleMovePicker(slotIndex: Int) {
selectedSlotIndex = slotIndex
if (movePickerOpen && pickerSlotIndex == slotIndex) {
closeMovePicker()
return
}
movePickerOpen = true
pickerSlotIndex = slotIndex
}
private fun closeMovePicker() {
movePickerOpen = false
pickerSlotIndex = -1
}
private fun assignMove(slotIndex: Int, move: MoveTemplate) {
draft.activeMoves.indices.forEach { index ->
if (index != slotIndex && draft.activeMoves[index] == move) {
draft.activeMoves[index] = null
}
}
draft.activeMoves[slotIndex] = move
}
private fun reorderMove(slotIndex: Int, up: Boolean) {
val size = draft.activeMoves.size
if (size == 0) return
val target = if (up) {
if (slotIndex > 0) slotIndex - 1 else size - 1
} else {
if (slotIndex < size - 1) slotIndex + 1 else 0
}
val temp = draft.activeMoves[target]
draft.activeMoves[target] = draft.activeMoves[slotIndex]
draft.activeMoves[slotIndex] = temp
if (selectedSlotIndex == slotIndex) selectedSlotIndex = target
else if (selectedSlotIndex == target) selectedSlotIndex = slotIndex
if (pickerSlotIndex == slotIndex) pickerSlotIndex = target
else if (pickerSlotIndex == target) pickerSlotIndex = slotIndex
}
private inner class MoveSlot(
pX: Int,
pY: Int,
private val slotIndex: Int
) : SoundlessWidget(pX, pY, moveWidthScaled, moveHeightScaled, Component.literal("move-slot-$slotIndex")) {
private val moveUpButton = ReorderMoveButton(x, y, true) {
reorderMove(slotIndex, true)
}.apply { addWidget(this) }
private val moveDownButton = ReorderMoveButton(x, y, false) {
reorderMove(slotIndex, false)
}.apply { addWidget(this) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val move = draft.activeMoves[slotIndex]
val matrices = context.pose()
isHovered = mouseX >= x && mouseY >= y && mouseX < x + width && mouseY < y + height
if (move != null) {
val rgb = move.elementalType.hue.toRGB()
if (selectedSlotIndex == slotIndex) {
blitk(
matrixStack = matrices,
texture = moveSelectedOverlayResource,
x = x - 1,
y = y - 1,
width = moveWidthScaled + 2,
height = moveHeightScaled + 2
)
}
blitk(
matrixStack = matrices,
texture = moveResource,
x = x,
y = y,
width = moveWidthScaled,
height = moveHeightScaled,
vOffset = if (isHovered) moveHeightScaled else 0,
textureHeight = moveHeightScaled * 2,
red = rgb.first,
green = rgb.second,
blue = rgb.third
)
blitk(
matrixStack = matrices,
texture = moveOverlayResource,
x = x,
y = y,
width = moveWidthScaled,
height = moveHeightScaled
)
val ppText = Component.literal("${move.pp}/${move.maxPp}").bold()
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = ppText,
x = x + (93 * BASE_SCALE),
y = y + (13 * BASE_SCALE),
centered = true,
scale = BASE_SCALE
)
TypeIcon(
x = x + (2 * BASE_SCALE),
y = y + (2 * BASE_SCALE),
type = move.elementalType
).render(context)
MoveCategoryIcon(
x = x + (66 * BASE_SCALE),
y = y + (13.5 * BASE_SCALE),
category = move.damageCategory
).render(context)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = move.displayName.bold(),
x = x + (28 * BASE_SCALE),
y = y + (2 * BASE_SCALE),
shadow = true,
scale = BASE_SCALE
)
moveUpButton.render(context, mouseX, mouseY, delta)
moveDownButton.render(context, mouseX, mouseY, delta)
}
renderSwapButton(context, mouseX, mouseY, move)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isSwapHovered(mouseX, mouseY)) {
toggleMovePicker(slotIndex)
return true
}
if (isHovered) {
selectedSlotIndex = slotIndex
}
return super.mouseClicked(mouseX, mouseY, button)
}
private fun renderSwapButton(context: GuiGraphics, mouseX: Int, mouseY: Int, move: MoveTemplate?) {
val selected = movePickerOpen && pickerSlotIndex == slotIndex
val swapOffsetX = 114.5F * BASE_SCALE
val swapOffsetY = 6.5F * BASE_SCALE
val swapWidth = 12F * BASE_SCALE
val swapHeight = 18F * BASE_SCALE
blitk(
matrixStack = context.pose(),
texture = if (move == null) moveAddButtonResource else moveSwapButtonResource,
x = (x + swapOffsetX) / SCALE,
y = (y + swapOffsetY) / SCALE,
width = swapWidth,
height = swapHeight,
vOffset = if (isSwapHovered(mouseX.toDouble(), mouseY.toDouble()) || selected) swapHeight else 0F,
textureHeight = swapHeight * 2,
scale = SCALE
)
}
private fun isSwapHovered(mouseX: Double, mouseY: Double): Boolean {
val swapOffsetX = 114.5F * BASE_SCALE
val swapOffsetY = 6.5F * BASE_SCALE
val swapWidth = 12F * BASE_SCALE
val swapHeight = 18F * BASE_SCALE
val swapX = x + swapOffsetX
val swapY = y + swapOffsetY
return mouseX.toFloat() in (swapX..(swapX + (swapWidth * SCALE)))
&& mouseY.toFloat() in (swapY..(swapY + (swapHeight * SCALE) - 0.5F))
}
}
private inner class MovePickerList(
listX: Int,
listY: Int
) : SummaryScrollList<MovePickerList.MoveEntry>(
listX,
listY,
lang("ui.moves.switch"),
MOVE_LIST_SLOT_HEIGHT + MOVE_LIST_SLOT_SPACING
) {
private var entries: List<MoveTemplate> = emptyList()
fun setEntries(moves: List<MoveTemplate>) {
clearEntries()
entries = moves
moves.forEach { move -> addEntry(MoveEntry(move)) }
scrollAmount = 0.0
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) {
if (!movePickerOpen) return
super.renderWidget(context, mouseX, mouseY, partialTicks)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (!movePickerOpen) return false
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
if (!movePickerOpen) return false
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (!movePickerOpen) return false
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
inner class MoveEntry(private val move: MoveTemplate) : Entry<MoveEntry>() {
override fun getNarration(): MutableComponent = move.displayName.copy()
override fun render(
context: GuiGraphics,
index: Int,
rowTop: Int,
rowLeft: Int,
rowWidth: Int,
rowHeight: Int,
mouseX: Int,
mouseY: Int,
isHovered: Boolean,
partialTicks: Float
) {
val matrices = context.pose()
val tweakedRowTop = rowTop - (MOVE_LIST_SLOT_SPACING / 2) + 1
val rgb = move.elementalType.hue.toRGB()
blitk(
matrixStack = matrices,
texture = moveListResource,
x = rowLeft,
y = tweakedRowTop,
height = MOVE_LIST_SLOT_HEIGHT,
width = rowWidth,
vOffset = if (isHovered) MOVE_LIST_SLOT_HEIGHT else 0,
textureHeight = MOVE_LIST_SLOT_HEIGHT * 2,
red = rgb.first,
green = rgb.second,
blue = rgb.third
)
blitk(
matrixStack = matrices,
texture = moveListOverlayResource,
x = rowLeft,
y = tweakedRowTop,
height = MOVE_LIST_SLOT_HEIGHT,
width = rowWidth
)
TypeIcon(
x = rowLeft - 9,
y = tweakedRowTop,
type = move.elementalType
).render(context)
MoveCategoryIcon(
x = rowLeft + 77,
y = tweakedRowTop + 1.5,
category = move.damageCategory
).render(context)
drawScaledText(
context = context,
text = move.displayName,
x = rowLeft + 14,
y = tweakedRowTop + 3.5,
scale = SCALE,
shadow = true
)
blitk(
matrixStack = matrices,
texture = movesPowerIconResource,
x = (rowLeft + 10) / SCALE,
y = (tweakedRowTop + 11) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
blitk(
matrixStack = matrices,
texture = movesAccuracyIconResource,
x = (rowLeft + 30) / SCALE,
y = (tweakedRowTop + 11) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
blitk(
matrixStack = matrices,
texture = movesEffectIconResource,
x = (rowLeft + 53.5) / SCALE,
y = (tweakedRowTop + 11) / SCALE,
width = 10,
height = 10,
scale = SCALE
)
val movePower = if (move.power.toInt() > 0) move.power.toInt().toString() else "-"
drawScaledText(
context = context,
text = Component.literal(movePower),
x = rowLeft + 16.5,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
drawScaledText(
context = context,
text = Component.literal(formatPercent(move.accuracy)),
x = rowLeft + 37,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
drawScaledText(
context = context,
text = Component.literal(formatPercent(move.effectChances.firstOrNull() ?: 0.0)),
x = rowLeft + 60.5,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
val pp = move.maxPp
drawScaledText(
context = context,
text = lang("ui.moves.pp", pp),
x = rowLeft + 76,
y = tweakedRowTop + 12,
scale = SCALE,
shadow = true
)
}
override fun mouseClicked(d: Double, e: Double, i: Int): Boolean {
if (isMouseOver(d, e) && pickerSlotIndex >= 0) {
assignMove(pickerSlotIndex, move)
selectedSlotIndex = pickerSlotIndex
closeMovePicker()
return true
}
return false
}
}
}
}
@@ -1,212 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_BASE_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SPACING
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
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.Mth
import org.joml.Quaternionf
import org.joml.Vector3f
class PokeEditSpeciesList(
val pX: Int,
val pY: Int,
val onSelect: (Species) -> Unit
) : ScrollingWidget<PokeEditSpeciesList.SpeciesRow>(
width = 139,
height = SCROLL_BASE_HEIGHT,
left = pX,
top = pY - SCROLL_BASE_HEIGHT,
slotHeight = SCROLL_SLOT_SIZE + 2
) {
private var entries: List<Species> = emptyList()
fun setEntries(species: List<Species>) {
clearEntries()
entries = species
species.chunked(5).forEach { chunk ->
addEntry(SpeciesRow(chunk, onSelect))
}
scrollAmount = 0.0
}
override fun addEntry(entry: SpeciesRow): Int = super.addEntry(entry)
override fun getScrollbarPosition(): Int = pX + 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 + 3, xRight, this.bottom - 3, 0xFF3A96B6.toInt())
context.fill(xLeft, yTop + 3, xRight, yTop + yBottom - 3, 0xFFFCFCFC.toInt())
}
class SpeciesRow(
private val speciesList: List<Species>,
private val onSelect: (Species) -> Unit
) : Slot<SpeciesRow>() {
companion object {
private val slotResource = cobblemonResource("textures/gui/pokedex/pokedex_slot.png")
private val slotHighlight = cobblemonResource("textures/gui/pokedex/slot_select.png")
}
private val runtime = MoLangRuntime().setupClient().setup().also {
it.environment.query.addFunction("get_pokedex") { null }
}
private var x: Int = 0
private var y: Int = 0
override fun render(
context: GuiGraphics,
index: Int,
y: Int,
x: Int,
entryWidth: Int,
entryHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
tickDelta: Float
) {
this.x = x
this.y = y
speciesList.forEachIndexed { indexInRow, species ->
val startPosX = x + ((SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * indexInRow)
val startPosY = y + SCROLL_SLOT_SPACING + 1
blitk(
matrixStack = context.pose(),
texture = slotResource,
x = startPosX,
y = startPosY,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE
)
if (getHoveredSlotIndex(mouseX, mouseY) == indexInRow) {
blitk(
matrixStack = context.pose(),
texture = slotHighlight,
x = startPosX,
y = startPosY,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE,
vOffset = SCROLL_SLOT_SIZE,
textureHeight = SCROLL_SLOT_SIZE * 2
)
}
val form = species.standardForm
val aspects = mutableSetOf<String>()
aspects.addAll(form.aspects)
if (form.maleRatio in 0F..1F) {
aspects.add(Gender.MALE.name.lowercase())
} else {
aspects.add(Gender.GENDERLESS.name.lowercase())
}
context.enableScissor(
startPosX + 1,
startPosY + 1,
startPosX + SCROLL_SLOT_SIZE - 1,
startPosY + SCROLL_SLOT_SIZE - 2
)
val state = FloatingState().also { it.currentAspects = aspects }
val matrices = context.pose()
matrices.pushPose()
matrices.translate(startPosX + (SCROLL_SLOT_SIZE / 2.0), startPosY + 1.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
drawProfilePokemon(
renderablePokemon = RenderablePokemon(species, aspects),
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
partialTicks = 0F,
scale = 4.5F
)
matrices.popPose()
context.disableScissor()
matrices.pushPose()
matrices.translate(0.0, 0.0, 100.0)
val pokemonNumber = species.nationalPokedexNumber.toString().padStart(4, '0').text()
drawScaledText(
context = context,
text = pokemonNumber,
x = startPosX + 1.5,
y = startPosY + 2.5,
shadow = true,
scale = SCALE
)
matrices.popPose()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val hoverIndex = getHoveredSlotIndex(mouseX.toInt(), mouseY.toInt())
if (hoverIndex in speciesList.indices) {
onSelect(speciesList[hoverIndex])
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(com.cobblemon.mod.common.CobblemonSounds.POKEDEX_CLICK, 1.0F))
}
return true
}
private fun getHoveredSlotIndex(mouseX: Int, mouseY: Int): Int {
speciesList.forEachIndexed { index, _ ->
val startPosX = x + ((SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * index)
val startPosY = y + SCROLL_SLOT_SPACING + 1
if (mouseX in startPosX..(startPosX + SCROLL_SLOT_SIZE)
&& mouseY in startPosY..(startPosY + SCROLL_SLOT_SIZE)
) {
return index
}
}
return -1
}
override fun getNarration(): Component {
return if (speciesList.isNotEmpty()) {
"${speciesList.first().name}-${speciesList.last().name}".text()
} else {
"".text()
}
}
}
}
@@ -1,445 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokeedit.widgets
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
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.pokeedit.PokeEditDraft
import com.cobblemon.mod.common.client.gui.pokeedit.widgets.PokeEditAspectsTab.Companion.choiceAspectId
import com.cobblemon.mod.common.client.gui.summary.SummaryButton
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.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.util.resolveFloat
import com.mojang.math.Axis
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.platform.Lighting
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Blocks
import org.joml.Quaternionf
import org.joml.Vector3f
import java.util.Locale
class PokeEditWalkSpeedTab(
pX: Int,
pY: Int,
private val draft: PokeEditDraft
) : SoundlessWidget(pX, pY, HALF_OVERLAY_WIDTH, HALF_OVERLAY_HEIGHT, Component.literal("Walk Speed")) {
companion object {
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val pokeBallOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_poke_ball.png")
private val platformBase = cobblemonResource("textures/gui/pokedex/platform_base.png")
private val platformShadow = cobblemonResource("textures/gui/pokedex/platform_shadow.png")
private val actionButtonResource = cobblemonResource("textures/gui/summary/summary_evolve_button.png")
private const val POKEBALL_OVERLAY_Y = 25
private const val PLATFORM_Y = 69
private const val SHADOW_X = 25
private const val SHADOW_Y = 76
private const val VIEW_SCALE = 1.2F
private const val VIEW_SHIFT_X = 0F
private const val VIEW_SHIFT_Y = 36F
private const val MODEL_VIEW_SCALE = 2.4F
private const val MODEL_DRAW_SCALE = 24F
private const val MODEL_OFFSET_Y = 20.0
private const val SPEED_INPUT_WIDTH = 40
private const val SPEED_INPUT_HEIGHT = 10
private const val SPEED_LABEL_SCALE = 0.5F
private const val BUTTON_WIDTH = 54
private const val BUTTON_HEIGHT = 15
private const val BLOCK_SPACING = 1.0F
private const val BLOCK_PLANE_OFFSET = 2.3
private const val BLOCK_COLUMNS = 7
private const val BLOCK_ROWS = 3
}
private val state = FloatingState().also { state ->
state.runtime.environment.query.addFunction("is_holding_item") {
val held = currentHeldStack()
DoubleValue(!held.isEmpty && !held.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) && !held.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_hat") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS))
}
state.runtime.environment.query.addFunction("is_wearing_face") {
DoubleValue(currentHeldStack().`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS))
}
}
private val heldItemRenderer = HeldItemRenderer()
private var renderablePokemon: RenderablePokemon? = null
private var rotationY = 30F
private var poseList: Array<PoseType> = arrayOf(PoseType.WALK, PoseType.PROFILE)
private var selectedPoseIndex: Int = 0
private var oldDragPosX = 0.0
private var canDragRender = false
private var isDraggingRender = false
private var cachedSpeedKey: String? = null
private var defaultWalkSpeed = 0.35F
private var customWalkSpeed: Float? = null
private var syncingSpeedInput = false
private val walkSpeedInput = EditBox(
Minecraft.getInstance().font,
pX + 6,
pY + 18,
SPEED_INPUT_WIDTH,
SPEED_INPUT_HEIGHT,
Component.literal("speed")
).also { editBox ->
editBox.setBordered(false)
editBox.setTextColor(0xFFFFFF)
editBox.setTextColorUneditable(0xFFFFFF)
editBox.setMaxLength(6)
editBox.active = true
editBox.visible = true
editBox.setFilter { value ->
value.isBlank() || value.matches(Regex("\\d*(\\.\\d*)?"))
}
editBox.setResponder { value ->
if (syncingSpeedInput) return@setResponder
customWalkSpeed = value.toFloatOrNull()
}
addWidget(editBox)
}
private val resetSpeedButton = SummaryButton(
buttonX = pX + 52F,
buttonY = pY + 14F,
buttonWidth = BUTTON_WIDTH,
buttonHeight = BUTTON_HEIGHT,
clickAction = { resetWalkSpeed() },
text = Component.literal("Reset"),
resource = actionButtonResource,
largeText = false,
boldText = false,
textScale = 0.7F
).also { addWidget(it) }
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
refreshDefaultWalkSpeed()
walkSpeedInput.active = true
walkSpeedInput.visible = true
if (!walkSpeedInput.isFocused && customWalkSpeed == null) {
val formatted = formatSpeed(defaultWalkSpeed)
if (walkSpeedInput.value != formatted) {
syncingSpeedInput = true
walkSpeedInput.value = formatted
syncingSpeedInput = false
}
}
val matrices = context.pose()
context.enableScissor(x, y, x + HALF_OVERLAY_WIDTH, y + HALF_OVERLAY_HEIGHT)
val viewWidth = HALF_OVERLAY_WIDTH * VIEW_SCALE
val viewHeight = HALF_OVERLAY_HEIGHT * VIEW_SCALE
val viewX = x + (HALF_OVERLAY_WIDTH - viewWidth) / 2F + VIEW_SHIFT_X
val viewY = y + (HALF_OVERLAY_HEIGHT - viewHeight) / 2F + VIEW_SHIFT_Y
val scaledBaseX = viewX / VIEW_SCALE
val scaledBaseY = viewY / VIEW_SCALE
blitk(
matrixStack = matrices,
texture = backgroundOverlay,
x = scaledBaseX,
y = scaledBaseY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = pokeBallOverlay,
x = scaledBaseX + 15,
y = scaledBaseY + POKEBALL_OVERLAY_Y,
width = 109,
height = 68,
vOffset = 0,
textureHeight = 1744,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformBase,
x = scaledBaseX + 13,
y = scaledBaseY + PLATFORM_Y,
width = 113,
height = 24,
textureHeight = 30,
scale = VIEW_SCALE
)
blitk(
matrixStack = matrices,
texture = platformShadow,
x = scaledBaseX + SHADOW_X,
y = scaledBaseY + SHADOW_Y,
width = 90,
height = 20,
scale = VIEW_SCALE
)
val fillTop = (viewY + 114F * VIEW_SCALE).toInt()
val fillBottom = (viewY + (HALF_OVERLAY_HEIGHT - 6) * VIEW_SCALE).toInt()
context.fill(viewX.toInt(), fillTop, (viewX + viewWidth).toInt(), fillBottom, 0xFF7AEEF1.toInt())
context.disableScissor()
walkSpeedInput.render(context, mouseX, mouseY, delta)
resetSpeedButton.render(context, mouseX, mouseY, delta)
drawScaledText(
context = context,
text = Component.literal("Walk Speed"),
x = x + 6,
y = y + 6,
scale = SPEED_LABEL_SCALE,
colour = 0xFFFFFFFF.toInt(),
shadow = false
)
buildRenderable()?.let { renderable ->
state.currentAspects = renderable.aspects
context.enableScissor(
x + 1,
y + 1,
x + HALF_OVERLAY_WIDTH - 1,
y + HALF_OVERLAY_HEIGHT - 1
)
matrices.pushPose()
matrices.translate(
x.toDouble() + (HALF_OVERLAY_WIDTH.toDouble()) / 2,
y.toDouble() + MODEL_OFFSET_Y,
1000.0
)
matrices.scale(MODEL_VIEW_SCALE, MODEL_VIEW_SCALE, MODEL_VIEW_SCALE)
renderWalkBlocks(context, renderable, effectiveWalkSpeed(), delta)
val rotationVector = Vector3f(13F, rotationY, 0F)
drawProfilePokemon(
renderablePokemon = renderable,
poseType = poseList[selectedPoseIndex],
matrixStack = matrices,
partialTicks = delta,
rotation = Quaternionf().fromEulerXYZDegrees(rotationVector),
state = state,
scale = MODEL_DRAW_SCALE
)
if (!renderable.heldItem.isEmpty) {
heldItemRenderer.renderOnModel(
renderable.heldItem,
state,
matrices,
context.bufferSource(),
light = 0xF000F0,
frontLight = true
)
}
matrices.popPose()
context.disableScissor()
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (isWithinViewport(mouseX, mouseY)) {
canDragRender = true
isDraggingRender = true
oldDragPosX = mouseX
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) {
canDragRender = false
}
if (isDraggingRender) {
isDraggingRender = false
}
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val handledDrag = isDraggingRender && canDragRender
if (handledDrag) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
rotationY = (((rotationY + dragOffsetY) % 360 + 360) % 360)
}
oldDragPosX = mouseX
return handledDrag || super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun resetWalkSpeed() {
customWalkSpeed = null
syncingSpeedInput = true
walkSpeedInput.value = formatSpeed(defaultWalkSpeed)
syncingSpeedInput = false
}
private fun refreshDefaultWalkSpeed() {
val key = "${draft.species.resourceIdentifier}#${draft.form.formOnlyShowdownId()}"
if (key == cachedSpeedKey) return
cachedSpeedKey = key
defaultWalkSpeed = resolveDefaultWalkSpeed()
if (customWalkSpeed == null) {
syncingSpeedInput = true
walkSpeedInput.value = formatSpeed(defaultWalkSpeed)
syncingSpeedInput = false
}
}
private fun resolveDefaultWalkSpeed(): Float {
return try {
state.runtime.resolveFloat(draft.form.behaviour.moving.walk.walkSpeed).coerceAtLeast(0.0F)
} catch (_: Exception) {
0.35F
}
}
private fun effectiveWalkSpeed(): Float {
val speed = customWalkSpeed ?: defaultWalkSpeed
return if (speed.isFinite() && speed > 0F) speed else defaultWalkSpeed
}
private fun formatSpeed(value: Float): String = String.format(Locale.US, "%.2f", value)
private fun renderWalkBlocks(context: GuiGraphics, renderable: RenderablePokemon, speed: Float, delta: Float) {
val model = VaryingModelRepository.getPoser(renderable.species.resourceIdentifier, state)
val ticks = (Minecraft.getInstance().level?.gameTime ?: (System.currentTimeMillis() / 50L)).toFloat() + delta
val offset = (ticks * speed) % BLOCK_SPACING
val travel = offset
val halfCols = BLOCK_COLUMNS / 2
val halfRows = BLOCK_ROWS / 2
val blockState = Blocks.GRASS_BLOCK.defaultBlockState()
val bufferSource = Minecraft.getInstance().renderBuffers().bufferSource()
val packedLight = LightTexture.pack(15, 15)
val hitboxHeight = draft.form.hitbox.height * draft.form.baseScale
val blockPlaneY = (-hitboxHeight * 0.5F + BLOCK_PLANE_OFFSET).toDouble()
val matrices = context.pose()
matrices.pushPose()
matrices.scale(MODEL_DRAW_SCALE, MODEL_DRAW_SCALE, -MODEL_DRAW_SCALE)
matrices.translate(
model.profileTranslation.x,
model.profileTranslation.y + 1.5 * model.profileScale,
model.profileTranslation.z - 4.0
)
matrices.scale(model.profileScale, model.profileScale, 1 / model.profileScale)
matrices.translate(0.0, blockPlaneY, 0.0)
val rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, rotationY, 0F))
matrices.mulPose(rotation)
RenderSystem.applyModelViewMatrix()
RenderSystem.enableDepthTest()
RenderSystem.disableCull()
Lighting.setupForEntityInInventory()
for (xIndex in -halfRows..halfRows) {
for (zIndex in -halfCols..halfCols) {
val z = zIndex * BLOCK_SPACING + travel
matrices.pushPose()
matrices.translate((xIndex * BLOCK_SPACING).toDouble(), 0.0, z.toDouble())
matrices.mulPose(Axis.XP.rotationDegrees(180F))
Minecraft.getInstance().blockRenderer.renderSingleBlock(
blockState,
matrices,
bufferSource,
packedLight,
OverlayTexture.NO_OVERLAY
)
matrices.popPose()
}
}
bufferSource.endBatch()
Lighting.setupFor3DItems()
RenderSystem.enableCull()
matrices.popPose()
}
private fun buildRenderable(): RenderablePokemon? {
val species = draft.species
val form = draft.form
val aspects = mutableSetOf<String>()
if (draft.shiny) aspects.add(SHINY_ASPECT.aspect)
aspects.addAll(form.aspects)
aspects.addAll(draft.forcedAspects)
aspects.addAll(draft.flagFeatures)
aspects.addAll(draft.cosmeticAspects)
draft.choiceFeatures.forEach { (key, choice) ->
choiceAspectId(key, choice)?.let { aspects.add(it) }
}
when (draft.gender) {
Gender.MALE -> aspects.add("male")
Gender.FEMALE -> aspects.add("female")
Gender.GENDERLESS -> aspects.add("genderless")
}
val heldStack = draft.heldItem
?.let { ResourceLocation.tryParse(it) }
?.let { id -> BuiltInRegistries.ITEM.getOptional(id).orElse(null) }
?.let { item -> ItemStack(item) }
?: ItemStack.EMPTY
renderablePokemon = RenderablePokemon(species, aspects, heldStack).also { recalculatePoses(it) }
return renderablePokemon
}
private fun recalculatePoses(renderablePokemon: RenderablePokemon) {
val poseState = FloatingState().also { it.currentAspects = renderablePokemon.aspects }
val poser = VaryingModelRepository.getPoser(renderablePokemon.species.resourceIdentifier, poseState)
poseList = poser.poses
.map { it.value.poseTypes.minBy { it.ordinal } }
.toSet()
.filterNot { it in PoseType.SHOULDER_POSES }
.sortedBy { it.ordinal }
.toTypedArray()
if (PoseType.WALK in poseList) {
selectedPoseIndex = poseList.indexOf(PoseType.WALK)
} else if (selectedPoseIndex !in poseList.indices) {
selectedPoseIndex = 0
}
}
private fun currentHeldStack(): ItemStack {
return renderablePokemon?.heldItem ?: ItemStack.EMPTY
}
private fun isWithinViewport(mouseX: Double, mouseY: Double): Boolean =
mouseX.toInt() in x..(x + HALF_OVERLAY_WIDTH)
&& mouseY.toInt() in y..(y + HALF_OVERLAY_HEIGHT)
&& children.none { child ->
child is AbstractWidget && child.visible && child.isMouseOver(mouseX, mouseY)
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.debug
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.render.debug.LineState
import com.cobblemon.mod.common.client.render.debug.SteerForceRenderer
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import net.minecraft.client.Minecraft
object S2CUpdateObsAvoidHandler : ClientNetworkPacketHandler<ClientboundUpdateObsAvoidPacket> {
override fun handle(packet: ClientboundUpdateObsAvoidPacket, client: Minecraft) {
client.executeIfPossible {
//ObjectAvoidanceRenderer.avoidanceLines[packet.entityId] = packet.lines
val lineState = SteerForceRenderer.lineStates.getOrPut(packet.entityId) { LineState() }
lineState.update(packet.lines)
}
}
}
@@ -0,0 +1,116 @@
/*
* 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.debug
import com.cobblemon.mod.common.api.net.Encodable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import net.minecraft.client.Camera
import net.minecraft.client.Minecraft
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
/**
* Data class currently used for storing line data for rendering in [SteerForceRenderer]
*
* @author Jackson
* @since September 18th, 2025
*/
data class EntityDebugLine(
val vector: Vec3, // Direction/magnitude in world space, relative to entity origin
val r: Float,
val g: Float,
val b: Float,
val a: Float,
) : Encodable {
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeVec3(vector)
buffer.writeFloat(r)
buffer.writeFloat(g)
buffer.writeFloat(b)
buffer.writeFloat(a)
}
companion object {
operator fun invoke(vector: Vec3, r: Float = 1f, g: Float = 1f, b: Float = 1f, a: Float = 1f): EntityDebugLine {
return EntityDebugLine(vector, r.coerceIn(0f,1f), g.coerceIn(0f,1f), b.coerceIn(0f,1f), a.coerceIn(0f,1f))
}
fun decode(buffer: RegistryFriendlyByteBuf): EntityDebugLine {
val vector = buffer.readVec3()
return EntityDebugLine(vector, buffer.readFloat(), buffer.readFloat(), buffer.readFloat(), buffer.readFloat())
}
}
}
class LineState {
/**
* The comparison of the previous and current lines relies on the order being consistent
*/
var previousLines: List<EntityDebugLine> = emptyList()
var currentLines: List<EntityDebugLine> = emptyList()
var lastReceivedLineTick: Long? = null
fun tick() {
previousLines = currentLines
}
fun update(newLines: List<EntityDebugLine>) {
lastReceivedLineTick = Minecraft.getInstance().level?.gameTime
currentLines = newLines
if (previousLines.isEmpty()) previousLines = newLines
}
// entityPos: interpolated world position of the entity this tick
fun draw(vc: VertexConsumer, pose: PoseStack.Pose, partialTick: Float, camera: Camera, entity: PokemonEntity, partialTicks: Float) {
previousLines.zip(currentLines).forEach { (prev, curr) ->
val iVec = prev.vector.lerp(curr.vector, partialTick.toDouble())
val iR = prev.r.lerp(curr.r, partialTick)
val iG = prev.g.lerp(curr.g, partialTick)
val iB = prev.b.lerp(curr.b, partialTick)
val iA = prev.a.lerp(curr.a, partialTick)
// Interpolated world position — same method used everywhere in the renderer
val worldStart = Vec3(
Mth.lerp(partialTicks.toDouble(), entity.xOld, entity.x),
Mth.lerp(partialTicks.toDouble(), entity.yOld, entity.y),
Mth.lerp(partialTicks.toDouble(), entity.zOld, entity.z)
)
val worldEnd = worldStart.add(iVec)
val relStart = worldStart.subtract(camera.position)
val relEnd = worldEnd.subtract(camera.position)
// Normal is the normalized direction of the segment
val dir = iVec.normalize()
val nx = dir.x.toFloat()
val ny = dir.y.toFloat()
val nz = dir.z.toFloat()
vc.addVertex(pose.pose(), relStart.x.toFloat(), relStart.y.toFloat(), relStart.z.toFloat())
.setColor(iR, iG, iB, iA)
.setNormal(pose, nx, ny, nz)
vc.addVertex(pose.pose(), relEnd.x.toFloat(), relEnd.y.toFloat(), relEnd.z.toFloat())
.setColor(iR, iG, iB, iA)
.setNormal(pose, nx, ny, nz)
}
}
fun reset() {
previousLines = emptyList()
currentLines = emptyList()
}
}
private fun Float.lerp(end: Float, t: Float): Float = this + (end - this) * t.coerceIn(0f, 1f)
@@ -0,0 +1,58 @@
/*
* 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.debug
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Camera
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
object SteerForceRenderer {
// Lines to render attached to the uuid of entities.
val lineStates = mutableMapOf<Int, LineState>()
fun registerTickHandler() {
// Advance line States
PlatformEvents.CLIENT_TICK_PRE.subscribe {
lineStates.forEach { uuid, lineState ->
lineState.tick()
}
}
}
fun render(poseStack: PoseStack, multiBufferSource: MultiBufferSource, partialTicks: Float, camera: Camera) {
val matrix = poseStack.last().pose()
val level = Minecraft.getInstance().level ?: return
val currTick = level.gameTime
val ticksUntilStale = 3
val keysToRemove = lineStates.keys.filter { entityId ->
val entity = level.getEntity(entityId) as? PokemonEntity
val lastRecTick = lineStates[entityId]?.lastReceivedLineTick
entity == null || (lastRecTick != null && (currTick - lastRecTick) > ticksUntilStale)
}
keysToRemove.forEach { lineStates.remove(it) }
// One buffer for all independent line segments
val lineBuffer = multiBufferSource.getBuffer(RenderType.lines())
lineStates.forEach { entityId, lineState ->
val entity = level.getEntity(entityId) as? PokemonEntity ?: return@forEach
lineState.draw(lineBuffer, poseStack.last(), partialTicks, camera, entity, partialTicks)
}
}
}
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.client.render.models.blockbench.pokemon.gen1
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PitchTiltAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.createTransformation
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BiWingedFrame
import com.cobblemon.mod.common.client.render.models.blockbench.frame.BipedFrame
@@ -198,6 +199,7 @@ class PidgeottoModel(root: ModelPart) : PokemonPosableModel(root), HeadedFrame,
transformTicks = 10,
quirks = arrayOf(blink, flyQuirk1, flyQuirk2),
animations = arrayOf(
PitchTiltAnimation(bone = getPart("body"), maxChangePerTick = 1F),
singleBoneLook(),
bedrock("pidgeotto", "air_fly")
)
@@ -146,6 +146,12 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Battles, lang = "experience_share_multiplier", SERVER)
var experienceShareMultiplier = .5
@CobblemonConfigField(Category.Battles, lang = "award_experience_to_fainted_pokemon", SERVER)
var awardExperienceToFaintedPokemon = false
@CobblemonConfigField(Category.Battles, lang = "award_experience_on_battle_loss", SERVER)
var awardExperienceOnBattleLoss = false
@CobblemonConfigField(Category.Battles, lang = "lucky_egg_multiplier", SERVER)
var luckyEggMultiplier = 1.5
@@ -14,9 +14,6 @@ import com.cobblemon.mod.common.CobblemonBehaviours
import com.cobblemon.mod.common.CobblemonCallbacks
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.CobblemonMechanics
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.CobblemonPartyCompositions
import com.cobblemon.mod.common.CobblemonPartyPools
import com.cobblemon.mod.common.CobblemonRideSettings
import com.cobblemon.mod.common.CobblemonUnlockableWallpapers
import com.cobblemon.mod.common.api.abilities.Abilities
@@ -81,15 +78,12 @@ object CobblemonDataProvider : DataProvider {
this.register(SpeciesFeatureAssignments, reloadable = false)
this.register(ActionEffects, reloadable = true)
this.register(Moves, reloadable = false)
this.register(CobblemonMovesetBuilders, reloadable = false)
this.register(Abilities, reloadable = false)
this.register(CobblemonBehaviours, reloadable = false)
this.register(PokemonSpecies, reloadable = false)
this.register(SpeciesAdditions, reloadable = false)
this.register(PokeBalls, reloadable = false)
this.register(PropertiesCompletionProvider, reloadable = false)
this.register(CobblemonPartyPools, reloadable = false)
this.register(CobblemonPartyCompositions, reloadable = false)
this.register(SpawnDetailPresets, reloadable = true)
this.register(CobblemonSpawnRules, reloadable = true)
this.register(CobblemonMechanics, reloadable = true)
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.entity.ai
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.ai.OmniPathNodeMaker
import com.cobblemon.mod.common.util.deleteNode
import com.cobblemon.mod.common.util.getWaterAndLavaIn
@@ -35,6 +34,7 @@ import net.minecraft.world.phys.Vec3
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.max
/**
* A navigator designed to work with the [OmniPathNodeMaker], allowing a path that can cross land, water, and air
@@ -49,6 +49,8 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
var cachedCurrentNode: Node? = null
var currentNodeDistance = 0F
var segmentIndex: Int = 0
var pathFollowTarget: Vec3? = null
data class NavigationContext(
val onHit: (damage: DamageSource) -> Unit = {},
@@ -160,6 +162,65 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
}
doStuckDetection(vec3d)
// Gather the steer target from the segment pathing
// TODO: this shouldn't live here
val currentPath = path ?: return
if (!currentPath.isDone) {
segmentIndex = segmentIndex.coerceIn(0, max(currentPath.nodeCount - 2, 0))
pathFollowTarget = getPathSteerTarget(currentPath, mob)
}
}
/**
* Gets a steering target for the current path
* This video explains it well: https://www.youtube.com/watch?v=rlZYT-uvmGQ
* Uses Reynolds path following for velocity steered entities
*/
fun getPathSteerTarget(
path: Path,
mob: Mob
): Vec3 {
// If this is the final segment then just seek towards the end
if (segmentIndex >= path.nodeCount - 1) return path.getNode(path.nodeCount - 1).asVec3().add(0.5, 0.0, 0.5)
// Determine future position to use in steering calculations
val lookAheadDist = 1.0 //TODO: Scale to look ahead a certain amount of ticks probably
val futurePos = mob.position().add(mob.deltaMovement.normalize().scale(lookAheadDist))
// Gather the current segment points
var pointS1 = path.getNode(segmentIndex).asVec3().add(0.5, 0.0, 0.5) // Add the 0.5's to get the blockface center as blockpos is on a corner
var pointS2 = path.getNode(segmentIndex + 1).asVec3().add(0.5, 0.0, 0.5)
// Project the future point onto the current segment
var vecA = futurePos.subtract(pointS1)
var vecB = pointS2.subtract(pointS1)
var normB = vecB.normalize()
val lenB = vecB.length()
var projAB = max(vecA.dot(normB), 0.0) // make at least 0 as negative means we are projecting behind the segment which we don't want
// If the projection goes off the front of the segment then advance to the next segment and recalculate the projection
if (projAB > lenB && segmentIndex < path.nodeCount - 2) {
segmentIndex++
pointS1 = path.getNode(segmentIndex).asVec3().add(0.5, 0.0, 0.5)
pointS2 = path.getNode(segmentIndex + 1).asVec3().add(0.5, 0.0, 0.5)
vecA = futurePos.subtract(pointS1)
vecB = pointS2.subtract(pointS1)
normB = vecB.normalize()
projAB = max(vecA.dot(normB), 0.0)
}
// Get the projected point on the segment
val projPoint = pointS1.add(normB.scale(projAB ))
// Determine if the future position is too far from the path by getting its distance from its projected position on the current path segment
val futureDistFromPath = projPoint.distanceTo(futurePos)
val acceptableDist = mob.bbWidth / 3.0 //TODO: figure out what this really needs to be
val pushForward = 0.1 // Advance the projected point along the segment to avoid going backwards
val seekPoint = if (futureDistFromPath < acceptableDist) futurePos.add(normB.scale(pushForward)) else projPoint.add(normB.scale(pushForward))
return seekPoint
}
fun isAirborne(world: Level, pos: BlockPos) =
@@ -223,6 +284,8 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
fun moveTo(x: Double, y: Double, z: Double, speed: Double = 1.0, navigationContext: NavigationContext) {
this.navigationContext = navigationContext
segmentIndex = 0
pathFollowTarget = null
this.moveTo(x, y, z, speed)
}
@@ -277,6 +340,8 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
}
override fun moveTo(path: Path?, speed: Double): Boolean {
segmentIndex = 0
pathFollowTarget = null
if (path != null && path.nodeCount > 0) {
val node = path.getNode(0)!!
// pokemonEntity.discard()
@@ -399,6 +464,8 @@ class OmniPathNavigation(val world: Level, val entity: Mob) : GroundPathNavigati
this.cachedCurrentNode = null
path = null
nodeEvaluator.done()
segmentIndex = 0
pathFollowTarget = null
// In case a path is cancelled instead of completed, check if we should stop flying
if (pather.couldStopFlying() && !isAirborne(entity.level(), entity.blockPosition())) {
pather.setFlying(false)
@@ -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.entity.ai.steering
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import net.minecraft.world.phys.Vec3
// Runs a list of SteerRules in priority order, consuming a force budget.
// High priority rules get their full contribution first. Lower priority
// rules only get what budget remains.
class SteerArbitrator(
val entity: PokemonEntity,
val maxForce: Double,
val rules: MutableList<SteerRule> = mutableListOf()
) {
private var sortedRules: List<SteerRule> = emptyList()
private var dirty = true
fun addRule(rule: SteerRule) { rules.add(rule); dirty = true }
fun removeRule(rule: SteerRule) { rules.remove(rule); dirty = true }
fun clearRules() { rules.clear(); dirty = true }
fun calculate(debug: Boolean = false): Vec3 {
if (dirty) {
sortedRules = rules.sortedByDescending { it.priority }
dirty = false
}
var remaining = maxForce
var result = Vec3.ZERO
val debugLines = if (debug) mutableListOf<EntityDebugLine>() else null
for (rule in sortedRules) {
if (remaining <= 0.0) break
val force = rule.calculate() ?: continue
val scaled = force.scale(rule.weight.toDouble())
val len = scaled.length()
if (len <= 0.0) continue
val contribution = if (len <= remaining) {
remaining -= len
scaled
} else {
val partial = remaining
remaining = 0.0
scaled.normalize().scale(partial)
}
result = result.add(contribution)
debugLines?.let {
val (r, g, b) = rule.debugColor
it.add(EntityDebugLine(contribution.scale(50.0), r, g, b, 1.0f))
}
}
debugLines?.let {
if (result.lengthSqr() > 0.0)
it.add(EntityDebugLine(result.scale(50.0), 1f, 1f, 1f, 1.5f))
if (it.isNotEmpty())
sendPacketToTrackingPlayers(entity, ClientboundUpdateObsAvoidPacket(entity.id, it))
}
return result
}
}
@@ -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.entity.ai.steering
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.world.phys.Vec3
// A per-tick steering force contributor evaluated by SteerArbitrator.
// Return null from calculate() if the rule has nothing to say this tick.
interface SteerRule {
val entity: PokemonEntity
// Higher = evaluated first, consumes force budget before lower priority rules.
// 90-100: obstacle avoidance
// 60-89: threat / boundary
// 30-59: social (separation, alignment, cohesion)
// 0-29: goal / wander
val priority: Int
// Multiplied against the rule output before budget accounting.
val weight: Float
// RGB 0-1 for the debug line drawn by SteerArbitrator.
val debugColor: Triple<Float, Float, Float> get() = Triple(1f, 1f, 1f)
fun calculate(): Vec3?
}
@@ -0,0 +1,76 @@
/*
* 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.entity.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.world.phys.Vec3
import kotlin.math.max
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.cos
// Forward AABB projection obstacle avoidance. Stateful — one instance per entity.
class ObstacleAvoidRule(
override val entity: PokemonEntity,
override val priority: Int = 100,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(1f, 0f, 0f),
val maxForce: Double = 0.02,
val lookAheadTicks: Int = 10,
val smoothingAlpha: Double = 0.25
) : SteerRule {
private var prevForce: Vec3 = Vec3.ZERO
override fun calculate(): Vec3? {
val topSpeed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
val forwardVec = if (entity.deltaMovement.horizontalDistanceSqr() < 0.001) {
val yaw = Math.toRadians(entity.yRot.toDouble())
Vec3(-sin(yaw), 0.0, cos(yaw))
} else {
Vec3(entity.deltaMovement.x, 0.0, entity.deltaMovement.z).normalize()
}
val lookAheadDist = max(entity.deltaMovement.length() * lookAheadTicks, entity.bbWidth / 2.0)
val collBox = entity.boundingBox.move(forwardVec.scale(lookAheadDist))
val shapes = entity.level().getBlockCollisions(entity, collBox)
val nearestShape = shapes.minByOrNull { shape ->
shape.bounds().center.distanceToSqr(entity.position())
}
if (nearestShape == null) {
prevForce = prevForce.lerp(Vec3.ZERO, smoothingAlpha)
return if (prevForce.lengthSqr() < 1e-6) null else prevForce
}
val obsCenter = nearestShape.bounds().center
val toObs = obsCenter.subtract(entity.position())
val projVec = forwardVec.scale(toObs.dot(forwardVec))
val lateralComponent = toObs.subtract(projVec)
val avoidForce = if (lateralComponent.lengthSqr() < 0.001) {
// Dead ahead — turn right
val rightVec = forwardVec.cross(Vec3(0.0, 1.0, 0.0))
entity.steerToTarget(entity.position().add(rightVec), topSpeed, maxForce, horzOnly = true)
} else {
var avoidanceVec = Vec3(lateralComponent.x * -1.0, 0.0, lateralComponent.z * -1.0)
val avoidStrength = sqrt(1.0 - (avoidanceVec.length() / (entity.bbWidth / 2.0 + 0.5)).coerceIn(0.0, 0.7))
val avoidancePoint = entity.position().add(avoidanceVec)
entity.steerToTarget(avoidancePoint, topSpeed, maxForce * avoidStrength, horzOnly = true)
}
prevForce = prevForce.lerp(avoidForce, smoothingAlpha)
return prevForce
}
}
@@ -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.entity.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.world.phys.Vec3
// Steers toward a target position. Reads speed from PokemonMoveControl.steerSpeed.
// Return null from targetProvider to deactivate for a tick.
class SeekRule(
override val entity: PokemonEntity,
override val priority: Int = 10,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(0f, 1f, 1f),
val maxForce: Double,
val horzOnly: Boolean = false,
val arrivalRange: Double? = null,
val targetProvider: (PokemonEntity) -> Vec3?
) : SteerRule {
override fun calculate(): Vec3? {
val target = targetProvider(entity) ?: return null
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
return entity.steerToTarget(target, speed, maxForce, arrivalRange, horzOnly)
}
}
@@ -0,0 +1,44 @@
/*
* 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.entity.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.world.phys.Vec3
import kotlin.math.sin
// Steers toward a point directly ahead of the entity's current velocity/yaw.
// Useful as a baseline test rule.
class WalkForwardRule(
override val entity: PokemonEntity,
override val priority: Int = 10,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(0f, 1f, 0f),
val maxForce: Double = 0.02
) : SteerRule {
override fun calculate(): Vec3? {
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
val forwardVec = if (entity.deltaMovement.horizontalDistanceSqr() < 0.001) {
val yaw = Math.toRadians(entity.yRot.toDouble())
Vec3(-sin(yaw), 0.0, cos(yaw))
} else {
Vec3(entity.deltaMovement.x, 0.0, entity.deltaMovement.z).normalize()
}
val pointAhead = entity.position().add(forwardVec)
return entity.steerToTarget(pointAhead, speed, maxForce, horzOnly = true)
}
}
private fun cos(d: Double) = kotlin.math.cos(d)
private fun sin(d: Double) = kotlin.math.sin(d)
@@ -0,0 +1,71 @@
/*
* 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.entity.ai.steering.rules
import com.cobblemon.mod.common.entity.ai.steering.SteerRule
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.ai.PokemonMoveControl
import com.cobblemon.mod.common.entity.pokemon.extensions.steerToTarget
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.sin
import kotlin.random.Random
// Reynolds wander — projects a sphere ahead and offsets a point on it each tick.
// Stateful: holds current wander angles, one instance per entity.
// https://slsdo.github.io/steering-behaviors/
class WanderRule(
override val entity: PokemonEntity,
override val priority: Int = 10,
override val weight: Float = 1.0f,
override val debugColor: Triple<Float, Float, Float> = Triple(1f, 0.5f, 0f),
val maxForce: Double = 0.02,
val wanderDisp: Double = 15.0,
val centerDistance: Double = 4.0,
val wanderRadius: Double = 4.0,
val horzOnly: Boolean = false //TODO: all of these should be checks if flying or walking
) : SteerRule {
var currHorzWander = 0.0
var currVertWander = 0.0
override fun calculate(): Vec3? {
val speed = (entity.moveControl as? PokemonMoveControl)?.steerSpeed ?: 0.1
val normVec = if (entity.deltaMovement.lengthSqr() > 0.0001) {
entity.lookAngle.normalize()
} else {
Vec3(
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0)
).normalize()
}
var wanderOff = normVec
// Clamp vertical wander when getting too steep
val currPitchS = sin(Mth.atan2(wanderOff.y, wanderOff.horizontalDistance()))
if (currPitchS >= 0.5f || currPitchS <= -0.5f) currVertWander = 0.0
wanderOff = wanderOff.xRot(Math.toRadians(currVertWander).toFloat())
wanderOff = wanderOff.yRot(Math.toRadians(currHorzWander).toFloat())
val wanderCenter = entity.position().add(normVec.scale(centerDistance))
val wanderPoint = wanderCenter.add(wanderOff.scale(wanderRadius))
// Advance wander angles
currVertWander = Mth.wrapDegrees(currVertWander + Random.nextDouble(-wanderDisp, wanderDisp))
currHorzWander = Mth.wrapDegrees(currHorzWander + Random.nextDouble(-wanderDisp, wanderDisp))
val force = entity.steerToTarget(wanderPoint, speed, maxForce)
return if (horzOnly) Vec3(force.x, 0.0, force.z) else force
}
}
@@ -191,7 +191,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
}
}
} else {
isNoGravity = false
setNoGravity(false)
deltaMovement = Vec3.ZERO
}
}
@@ -175,10 +175,7 @@ import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.math.PI
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.*
@Suppress("unused")
open class PokemonEntity(
@@ -214,6 +211,7 @@ open class PokemonEntity(
@JvmStatic var RIDE_BOOSTS = SynchedEntityData.defineId(PokemonEntity::class.java, RideBoostsDataSerializer)
@JvmStatic var RIDE_STAMINA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic var SCALE_MODIFIER = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic val IS_SPRINTING = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
const val BATTLE_LOCK = "battle"
const val EVOLUTION_LOCK = "evolving"
@@ -293,6 +291,9 @@ open class PokemonEntity(
var shownItem: ItemStack
get() = entityData.get(SHOWN_HELD_ITEM)
set(value) = entityData.set(SHOWN_HELD_ITEM, value)
var isPokemonSprinting: Boolean
get() = entityData.get(IS_SPRINTING)
set(value) { entityData.set(IS_SPRINTING, value) }
var lastLightningBoltUUID: UUID? = null
@@ -461,6 +462,7 @@ open class PokemonEntity(
builder.define(RIDE_BOOSTS, emptyMap())
builder.define(RIDE_STAMINA, 1F)
builder.define(SCALE_MODIFIER, 1F)
builder.define(IS_SPRINTING, false)
}
override fun onSyncedDataUpdated(data: EntityDataAccessor<*>) {
@@ -623,8 +625,12 @@ open class PokemonEntity(
isPokemonFlying = flyDist - flyDistO > 0.005F
isPokemonWalking = walkDist - walkDistO > 0.005F
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
// TODO: Figure out a better way to allow for this. Or maybe just always do the animations data accumulation its really not that bad
if (this.pokemon.species.name.lowercase() == "charizard" || (passengers.isNotEmpty() && level().isClientSide)) {
// This is so ugly why am I doing it like this... even if its for a test this is low
if (passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.tick()
}
ridingAnimationData.update()
} else if (!passengers.isNotEmpty() && level().isClientSide) {
rideSoundManager.stop()
@@ -2151,7 +2157,9 @@ open class PokemonEntity(
override fun stopSeenByPlayer(player: ServerPlayer) {
if (this.ownerUUID == player.uuid && tethering == null) {
// queuedToDespawn = true
this.remove(RemovalReason.DISCARDED)
if (this.removalReason != RemovalReason.DISCARDED) {
this.remove(RemovalReason.DISCARDED)
}
return
}
}
@@ -0,0 +1,547 @@
/*
* 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.entity.pokemon.ai
import com.cobblemon.mod.common.CobblemonActivities
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.extensions.*
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.resolveDouble
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.control.MoveControl
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.level.ClipContext
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import kotlin.math.*
import kotlin.random.Random
class BoidsMoveControl(val pokemonEntity: PokemonEntity, val target: LivingEntity) : MoveControl(pokemonEntity) {
companion object {
const val VERY_CLOSE = 2.500000277905201E-3
const val MAX_SPEED = 0.3 / 2
const val MAX_FORCE = 0.02
const val PREDATOR_MAX_SPEED = 0.6
// const val MAX_SPEED = 0.2
// const val MAX_FORCE = 0.013
}
// TODO: might need to hold its own velocity so minecraft doesn't do its nonsense to it?
// Can handle collisions ourselves as well
var speed: Double? = null
var drawLines: MutableList<EntityDebugLine> = mutableListOf()
// wander vars
var currHorzWander = 0.0
var currVertWander = 0.0
val wanderDisp = 15.0 // The degrees of change from tick to tick in wander angle
override fun tick() {
// Clear since the info is sent and is now stale
drawLines.clear()
// Steering constants
val orbitRadius = 16.0
val targetPos = target.position().add(0.0, 4.0, 0.0)
val arrivalRange = 2.0 // in blocks
val species = pokemonEntity.pokemon.species.name
// Set this to ensure the flying pose is active
// ignore all the friction stuff from outside
speed?.let { pokemonEntity.setDeltaMovement(pokemonEntity.deltaMovement.normalize().scale(it)) }
// Constrain to rectangular boundary
val cubeWidth = 16.0
val cubeHeight = 16.0
val cubeCenter = target.position().add(0.0, cubeHeight / 2.0, 0.0)
//forceRectBoundary(cubeCenter, cubeWidth, cubeHeight)
// Apply boid forces
// Obstacle Avoidance
val whiskerRange = 1.0
val avoidanceAngle = 20.0 // x degrees to the left, right, up, and down
//val obsAvoidForce = whisObsAvoid(whiskerRange, avoidanceAngle)
// if (obsAvoidForce != null) {
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(obsAvoidForce.scale(0.5))
// }
// TODO: obviously don't hardcode each pokemon. Remove this after testing is finished.
if (pokemonEntity.pokemon.species.name.lowercase() == "charizard") {
pokemonEntity.setFlying(true)
pokemonEntity.resetFallDistance()
seekPrey(pokemonEntity)
} else if (pokemonEntity.pokemon.species.name.lowercase() == "garchomp") {
pokemonEntity.setFlying(false)
val walkSpeed = pokemonEntity.runtime.resolveDouble(pokemonEntity.behaviour.moving.walk.walkSpeed) * 1.5
val steerForce = pokemonEntity.steerToTarget(target.position(), walkSpeed, walkSpeed / 12)
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce.x, 0.0, steerForce.z)
// speed?.let { this.mob.setZza(speed!!.toFloat()) }
} else if (pokemonEntity.pokemon.species.name.lowercase() == "skiddo") {
pokemonEntity.setFlying(false)
//wander(pokemonEntity,1.0)
// val steerForce = pokemonEntity.steerToTargetForce(target.position(), MAX_SPEED, MAX_FORCE)
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce.x, 0.0, steerForce.z)
aviodPredators(pokemonEntity, includePlayer = true)
boid(isFlyingOrSwimming = false)
} else {
pokemonEntity.setFlying(true)
pokemonEntity.resetFallDistance()
wander(pokemonEntity,0.8)
boid()
aviodPredators(pokemonEntity, includePlayer = true)
// Force towards ground
forceRectBoundary(pokemonEntity, cubeCenter, cubeWidth, cubeHeight)
}
// val avoidForce = avoidTarget(target.position(), MAX_SPEED, MAX_FORCE, 10.0)
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(avoidForce.scale(1.0))
// Apply steering force to current movement
// val dm = pokemonEntity.deltaMovement
// val w_pos = pokemonEntity.position().add(Vec3(dm.x, 0.0, dm.z))
// val steerForce = steerToTargetForce(w_pos, MAX_SPEED, MAX_FORCE)
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce)
// Take note of speed again to avoid minecraft's friction logic
speed = pokemonEntity.deltaMovement.length() * 0.98
// Test out what this setting of speed does.
//speed?.let { this.mob.speed = speed!!.toFloat() }
// Limit upwards speed to be half of max speed. The boids kept bumping into the top of the box more so than birds
// would. Honestly birds probably mostly want to move in the x,z plane anyhow
// pokemonEntity.deltaMovement = Vec3(
// pokemonEntity.deltaMovement.x,
// min(pokemonEntity.deltaMovement.y, MAX_SPEED * 0.5),
// pokemonEntity.deltaMovement.y
// ).normalize().scale(speed!!)
pokemonEntity.orientEntityToTravel(0.4f)
if (false && drawLines.isNotEmpty()) { //TODO: Re-enable
sendPacketToTrackingPlayers(pokemonEntity, ClientboundUpdateObsAvoidPacket(pokemonEntity.id, drawLines))
}
}
private fun aviodPredators(entity: PokemonEntity, includePlayer: Boolean = false) {
val fearRadius = 5.0
val brain = entity.brain
val predators = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS &&
pokemon.pokemon.species.name.lowercase() == "charizard" &&
entity.position().subtract(pokemon.position()).length() <= fearRadius
}
}.orElse(emptyList())
var totalEvasionVec = Vec3.ZERO
var predatorCount = predators.size
// If the player should be included in avoidance then add the player as a predator.
if (includePlayer) {
totalEvasionVec = totalEvasionVec.add(entity.avoidTarget(target.position(), MAX_SPEED * 1.2, MAX_FORCE * 2, fearRadius))
predatorCount += 1
}
// Average evasion forces for all predators in the area and apply it to the prey entity
if (predatorCount > 0) {
for (predator in predators) {
totalEvasionVec = totalEvasionVec.add(entity.avoidTarget(predator.position(), MAX_SPEED * 1.2, MAX_FORCE * 2, fearRadius))
}
totalEvasionVec = totalEvasionVec.scale(1.0 / predatorCount)
if (entity.isFlying() || entity.isSwimming) {
entity.deltaMovement = entity.deltaMovement.add(totalEvasionVec)
} else {
entity.deltaMovement = entity.deltaMovement.add(Vec3(totalEvasionVec.x, 0.0, totalEvasionVec.z))
}
}
}
/*************************************************************************************************************
Seek the closest prey and pursue
*************************************************************************************************************/
private fun seekPrey(predator: PokemonEntity) {
val SeekRadius = 20.0
val brain = predator.brain
// Do calcs for head height as it looks better this way than a charizard flying crotch first at a group of pidgeys
val kindaHeadHeight = predator.position()
val prey = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS &&
(pokemon.pokemon.species.name.lowercase() == "pidgey" || pokemon.pokemon.species.name.lowercase() == "skiddo") &&
kindaHeadHeight.subtract(pokemon.position()).length() <= SeekRadius
}
}.orElse(emptyList())
// Find the closest prey and seek towards it
val nearestPrey = prey.minByOrNull { pokePrey ->
kindaHeadHeight.subtract(pokePrey.position()).length()
}
nearestPrey?.let { prey ->
// Do the actual attack
// TODO: make this not dumb
if(kindaHeadHeight.subtract(nearestPrey.position()).length() < 0.5 + predator.bbWidth + nearestPrey.bbWidth) {
//predator.doHurtTarget(nearestPrey)
}
// Steer towards the target
val steerForce = predator.steerToTarget(prey.position(), PREDATOR_MAX_SPEED, MAX_FORCE)
predator.deltaMovement = predator.deltaMovement.add(steerForce)
}
}
/*************************************************************************************************************
Whisker Obstacle Avoidance
This function ray casts in a cone in front of the entity
*************************************************************************************************************/
private fun whisObsAvoid(range: Double, lookWidth: Double): Vec3? {
// --- 1. Build the Orientation Matrix ---
val forward = pokemonEntity.lookAngle.scale(range)
// val tempUp = if (abs(forward.y) > 0.999) Vec3(0.0, 0.0, 1.0) else Vec3(0.0, 1.0, 0.0)
// val right = forward.cross(tempUp).normalize()
// val up = right.cross(forward).normalize()
//
// val orientation = Matrix3f()
// .setColumn(0, Vector3f(right.x.toFloat(), right.y.toFloat(), right.z.toFloat()))
// .setColumn(1, Vector3f(up.x.toFloat(), up.y.toFloat(), up.z.toFloat()))
// .setColumn(2, Vector3f(forward.x.toFloat(), forward.y.toFloat(), forward.z.toFloat()))
// --- 2. Define Whiskers in Simple, Local Space ---
val radLookWidth = lookWidth.toRadians().toFloat()
// val localWhiskers: List<Vector3f> = listOf(
// Vector3f(0f, 0f, 1f), // Forward
//// Vector3f(sin(radLookWidth), 0f, cos(radLookWidth)), // Left
//// Vector3f(-sin(radLookWidth), 0f, cos(radLookWidth)), // Right
//// Vector3f(0f, sin(radLookWidth), cos(radLookWidth)), // Up
//// Vector3f(0f, -sin(radLookWidth), cos(radLookWidth)) // Down
// )
//
// // --- 3. Transform Local Whiskers to World Space using the Matrix ---
// val whiskerOffsets = localWhiskers.map { localVec ->
// orientation.transform(localVec, Vector3f()).toVec3d().scale(range) // Transform and store in a new vector
// }
// Obtain all points to be raycasted to
val scaledLookVec = pokemonEntity.deltaMovement.normalize().scale(range)
val lookPoints: List<Vec3> = listOf(
forward,
// forward.yRot(radLookWidth),
// forward.yRot(-radLookWidth),
// forward.xRot(90.toRadians()),
// forward.xRot((-90).toRadians()),
)
// iterate through each point and raycast to it starting from the entities pos (may need to be from somewhere else)
// If a position is hit then add its avoidance force to the totalAvoidance forces.
var totalAvoidance: Vec3? = null
val level = pokemonEntity.level()
val startPos = pokemonEntity.position()
for (point in lookPoints) {
val endPos = startPos.add(point)
val hit = level.clip(
ClipContext(
startPos,
endPos,
ClipContext.Block.OUTLINE, // If this is being weird then go back to COLLIDER
ClipContext.Fluid.ANY, // Birds should probably avoid fluid
pokemonEntity
)
)
// Add the whisker to the debug lines to be drawn
drawLines.add(EntityDebugLine(endPos, 1.0f, 0.0f, 0.0f))
//visualizeRay(level, startPos, endPos, hit.type != HitResult.Type.MISS)
if (hit.type != HitResult.Type.MISS) {
// Get the aviodance vector as the vector from the block detection point
// to the pokemon.
// Also inversely scale the size of the vector based on the distance the pokemon
// is to the object. This means close obstacles are avoided more strongly
val hitDist = hit.location.distanceTo(pokemonEntity.eyePosition)
val avoidVec = startPos.subtract(endPos).normalize() //.scale(1.0 - (hitDist / range))
// Add avoid vec to total
totalAvoidance = totalAvoidance?.add(avoidVec) ?: avoidVec
}
}
// // Scale all forces down to the max foce
// if (totalAvoidance != null) {
// // Cap the max total avoidance desire
// if (totalAvoidance.length() > MAX_SPEED) totalAvoidance = totalAvoidance.normalize().scale(MAX_SPEED)
// // Convert to Reynolds steering force
// totalAvoidance = totalAvoidance.subtract(pokemonEntity.deltaMovement)
// // Cap the max total avoidance force
// if (totalAvoidance.length() > MAX_FORCE) totalAvoidance = totalAvoidance.normalize().scale(MAX_FORCE)
// } // Is this next one even cleaner?
return totalAvoidance?.let { desiredVelocity ->
// Cap the desired velocity (using lengthSqr is more efficient than length)
val cappedVelocity = if (desiredVelocity.lengthSqr() > MAX_SPEED * MAX_SPEED) {
desiredVelocity.normalize().scale(MAX_SPEED)
} else {
desiredVelocity
}
// Convert to Reynolds steering force
val steeringForce = cappedVelocity.subtract(pokemonEntity.deltaMovement)
// Cap the final steering force
if (steeringForce.lengthSqr() > MAX_FORCE * MAX_FORCE) {
steeringForce.normalize().scale(MAX_FORCE)
} else {
steeringForce
}
}
}
private fun wander(entity: PokemonEntity, forceScale: Double = 1.0) {
// Wander behaviour
/*************************************************************************************************************
* The wander behaviour is based of Reynolds wander behaviour which can be seen in the below link under wander
* This version however is adapted to 3dimensions.
* https://slsdo.github.io/steering-behaviors/
*************************************************************************************************************/
// Get curr steering positions in left/right and up/down offsets
val normVec = if(entity.deltaMovement.lengthSqr() > 0.0001) {
entity.lookAngle.normalize()
} else {
Vec3(
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0),
Random.nextDouble(-1.0, 1.0)
).normalize()
}
var wanderOff = normVec
val currPitchS = sin(Mth.atan2(wanderOff.y, wanderOff.horizontalDistance()))
if (currPitchS >= 0.5f || currPitchS <= -0.5f) { //TODO: make this y limit class wide
this.currVertWander = 0.0
}
wanderOff = wanderOff.xRot(this.currVertWander.toRadians())
wanderOff = wanderOff.yRot(this.currHorzWander.toRadians())
// Base the center off the current direction of travel
val centerDistance = 4.0 // How many blocks the wander 'sphere' is in front of the entity
val wanderCenter = entity.position().add(normVec.scale(centerDistance))
// Get the wander circle's radius
val wanderRadius = 4.0
val wanderPoint = wanderCenter.add(wanderOff.scale(wanderRadius))
// change wander points
val randVert = Random.nextDouble(-wanderDisp, wanderDisp)
val randHorz = Random.nextDouble(-wanderDisp, wanderDisp)
currVertWander = Mth.wrapDegrees(randVert + currVertWander)
currHorzWander = Mth.wrapDegrees(randHorz + currHorzWander)
drawLines.add(EntityDebugLine(wanderPoint.subtract(pokemonEntity.position()), 1.0f, 0.0f, 0.0f, 1.0f)) // Add WanderPoint
drawLines.add(EntityDebugLine(wanderCenter.subtract(pokemonEntity.position()), 0.0f, 1.0f, 0.0f, 1.0f)) // Add wander center
val wanderForce = entity.steerToTarget(wanderPoint, MAX_SPEED, MAX_FORCE)
if(pokemonEntity.isFlying() || pokemonEntity.isSwimming) {
entity.deltaMovement = pokemonEntity.deltaMovement.add(wanderForce.scale(forceScale))
} else {
entity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(wanderForce.x, 0.0, wanderForce.z).scale(forceScale))
}
}
private fun boid(isFlyingOrSwimming: Boolean = true) {
// TODO: maybe move this to a sensor that lives spins up on the boid activity creation
// Get a list of other boid entities in this entity's vicinity
val brain = pokemonEntity.brain
val boids = brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).map { nearbyEntities ->
nearbyEntities.filterIsInstance<PokemonEntity>().filter { pokemon ->
pokemon.brain.activeNonCoreActivity.orElse(null) == CobblemonActivities.POKEMON_BOIDS
}
}.orElse(emptyList())
// separation
val separationForce = separation(boids)?.scale(0.8)
separationForce?.let { force ->
if( isFlyingOrSwimming) {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
} else {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
}
}
// alignment
val alignmentForce = alignment(boids)?.scale(0.0)
alignmentForce?.let { force ->
if( isFlyingOrSwimming) {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
} else {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
}
}
// cohesion
val cohesionForce = cohesion(boids)?.scale(0.6)
cohesionForce?.let { force ->
if( isFlyingOrSwimming) {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
} else {
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(force.x, 0.0, force.z))
}
}
}
private fun separation(boids: List<PokemonEntity>): Vec3? {
var seperationVec: Vec3? = null
// Gather the total separation vec of all close entities
boids.forEach { boid ->
val toTargetVec = pokemonEntity.position().subtract(boid.position())
val desiredSeperation = boid.bbWidth * 2.0 // Base the seperation distance on the model width
val currDist = toTargetVec.length()
if (currDist < desiredSeperation) {
if (seperationVec == null) seperationVec = Vec3.ZERO
// Make the seperation force inversely proprotional to the serperation distance
// Also, scale the force by the bbwidth of the model so
seperationVec = seperationVec.add(toTargetVec.normalize().scale((1.0 - (currDist / desiredSeperation)) ))
}
}
if (seperationVec != null) {
//if (seperationVec.length() > MAX_SPEED) seperationVec = seperationVec.normalize().scale(MAX_SPEED)
seperationVec = seperationVec.normalize().scale(MAX_SPEED)
seperationVec = seperationVec.subtract(pokemonEntity.deltaMovement)
if (seperationVec.length() > MAX_FORCE) {
seperationVec = seperationVec.normalize().scale(MAX_FORCE)
}
}
return seperationVec
}
private fun alignment(boids: List<PokemonEntity>): Vec3? {
val neighborDist = 4.0
var summedVel: Vec3? = null
var alignmentVec: Vec3? = null
var boidCount = 0
// Gather the average position of the boid
boids.forEach { boid ->
val vecToOther = boid.position().subtract(this.pokemonEntity.position())
val distToOther = vecToOther.length()
val widthOfViewInDeg = 180
val isInSightLine = pokemonEntity.deltaMovement.normalize().dot(vecToOther) > cos((widthOfViewInDeg / 2.0).toRadians())
if (isInSightLine && distToOther < neighborDist) {
if (summedVel == null) summedVel = Vec3.ZERO
summedVel = summedVel.add(boid.deltaMovement.normalize())
boidCount++
}
}
if (summedVel != null) {
// Get the average vel/alignment by dividing by the number of boids sampled
val averageVel = summedVel.scale(1.0 / boidCount)
//alignmentVec = if (averageVel.length() > MAX_SPEED) averageVel.normalize().scale(MAX_SPEED) else averageVel
alignmentVec = averageVel.normalize().scale(MAX_SPEED)
alignmentVec = alignmentVec.subtract(pokemonEntity.deltaMovement)
if (alignmentVec.length() > MAX_FORCE) {
alignmentVec = alignmentVec.normalize().scale(MAX_FORCE)
}
}
return alignmentVec
}
private fun cohesion(boids: List<PokemonEntity>): Vec3? {
//val neighborDist = 5.0
var summedPos: Vec3? = null
var cohesionVec: Vec3? = null
var boidCount = 0
val cohesionRadius = 8.0
// Gather the average position of the boid
boids.forEach { boid ->
val vecToOther = boid.position().subtract(this.pokemonEntity.position())
val distToOther = vecToOther.length()
val widthOfViewInDeg = 360
val isInSightLine = pokemonEntity.deltaMovement.normalize().dot(vecToOther) > cos((widthOfViewInDeg / 2.0).toRadians())
if (isInSightLine && distToOther < cohesionRadius) { // Only use members that are within a certain radius and eyeline
if (summedPos == null) summedPos = Vec3.ZERO
summedPos = summedPos.add(boid.position())
boidCount++
}
}
if (summedPos != null) {
// Get the average position by dividing by the number of boids sampled
val averagePos = summedPos.scale(1.0 / boidCount)
// Get the vector towards that average position and scale
cohesionVec = averagePos.subtract(pokemonEntity.position())
//if (cohesionVec.length() > MAX_SPEED) cohesionVec = cohesionVec.normalize().scale(MAX_SPEED)
cohesionVec = cohesionVec.normalize().scale(MAX_SPEED)
cohesionVec = cohesionVec.subtract(pokemonEntity.deltaMovement)
if (cohesionVec.length() > MAX_FORCE) {
cohesionVec = cohesionVec.normalize().scale(MAX_FORCE)
}
}
return cohesionVec
}
private fun forceRectBoundary(entity: PokemonEntity, basePos: Vec3, rectWidth: Double, rectHeight: Double) {
val maxBoundaryForce = MAX_FORCE //* 2.0
val correctiveForce = getRectCorrectiveForce(basePos, rectWidth, rectHeight)
if (correctiveForce != null) {
//val correctiveForce = steerToTargetForce(correctivePos, speedLimit, maxForce)
var steer = correctiveForce.subtract(entity.deltaMovement)
if (steer.length() > maxBoundaryForce) {
steer = steer.normalize().scale(maxBoundaryForce)
}
entity.setDeltaMovement(entity.deltaMovement.add(steer))
}
}
// Returns the desired force to correct the entity back into the rectangle area
private fun getRectCorrectiveForce( basePos: Vec3, rectWidth: Double, rectHeight: Double): Vec3? {
val entityPos = pokemonEntity.position()
val posFromBase = entityPos.subtract(basePos)
val cubeHalfWidth = rectWidth / 2.0 // Useful as the basePoint is in the middle of the cube
val cubeHalfHeight = rectHeight / 2.0
// Check the three possible faces for exceeding the boundary (x, y, and z faces of cube)
val xExceed = max(0.0, abs(posFromBase.x) - cubeHalfWidth)
val yExceed = max(0.0, abs(posFromBase.y) - cubeHalfHeight)
val zExceed = max(0.0, abs(posFromBase.z) - cubeHalfWidth)
return if ( xExceed == 0.0 && yExceed == 0.0 && zExceed == 0.0) {
null
} else {
var desiredPos = pokemonEntity.position().add(pokemonEntity.deltaMovement) // Add delta movement so we aren't slowing this guy down
if (xExceed != 0.0) desiredPos = Vec3( basePos.x, desiredPos.y, desiredPos.z)
if (yExceed != 0.0) desiredPos = Vec3( desiredPos.x, basePos.y, desiredPos.z)
if (zExceed != 0.0) desiredPos = Vec3( desiredPos.x, desiredPos.y, basePos.z)
return desiredPos.subtract(pokemonEntity.position()).normalize().scale(MAX_SPEED)
}
}
}
//val desiredScale = if (distToTarget > slowDownRadius) speedLimit else (distToTarget/slowDownRadius).pow(2) * speedLimit
//desiredVec = desiredVec.normalize().scale(desiredScale)
@@ -9,30 +9,47 @@
package com.cobblemon.mod.common.entity.pokemon.ai
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToTrackingPlayers
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine.Companion.invoke
import com.cobblemon.mod.common.entity.PlatformType
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.ai.OmniPathNavigation
import com.cobblemon.mod.common.entity.ai.steering.SteerArbitrator
import com.cobblemon.mod.common.entity.ai.steering.rules.ObstacleAvoidRule
import com.cobblemon.mod.common.entity.ai.steering.rules.SeekRule
import com.cobblemon.mod.common.entity.ai.steering.rules.WanderRule
import com.cobblemon.mod.common.entity.ai.steering.rules.WalkForwardRule
import com.cobblemon.mod.common.entity.pokemon.PokemonBehaviourFlag
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.extensions.*
import com.cobblemon.mod.common.net.messages.client.debug.ClientboundUpdateObsAvoidPacket
import com.cobblemon.mod.common.util.getWaterAndLavaIn
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.resolveFloat
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.tags.BlockTags
import net.minecraft.tags.FluidTags
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.entity.ai.control.MoveControl
import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.random.Random
class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemonEntity) {
companion object {
@@ -50,6 +67,41 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
var bankUpwardsBlocksPerTick = 0F
var bankRightDegreesPerTick = 1F
var obsCorrectDir = 0F
var drawLines: MutableList<EntityDebugLine> = mutableListOf()
var prevForce: Vec3? = null
var steerSpeed = this.gatherTopSpeed(pokemonEntity)
private val steerArbitrator by lazy {
val steerArbitrator = SteerArbitrator(
entity = pokemonEntity,
maxForce = 0.02,
rules = mutableListOf(
ObstacleAvoidRule(
entity = pokemonEntity,
debugColor = Triple(1f, 0f, 0f),
),
SeekRule(
entity = pokemonEntity,
priority = 10,
maxForce = 0.02,
debugColor = Triple(0f, 1f, 0f),
horzOnly = true,
targetProvider = { e ->
(e.navigation as? OmniPathNavigation)?.pathFollowTarget //TODO: maybe change this seems silly. Probably want a rule that checks explicitly?
}
),
WanderRule(
entity = pokemonEntity,
debugColor = Triple(0f, 1f, 1f),
horzOnly = true
)
)
)
steerArbitrator
}
fun startBanking(
forwardBlocksPerTick: Float = 0F,
upwardsBlocksPerTick: Float = 0F,
@@ -73,6 +125,7 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
override fun tick() {
mob.setDiscardFriction(false) // ignored while trying to move but if stopped or doing something else this needs reenabled
if (banking && operation != Operation.WAIT) {
banking = false
}
@@ -102,6 +155,7 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * this.speedModifier.toFloat()
val adjustedSpeed = baseSpeed * mediumSpeed
if (banking) {
pokemonEntity.yRot += bankRightDegreesPerTick
pokemonEntity.travel(Vec3(0.0, bankUpwardsBlocksPerTick.toDouble(), bankForwardBlocksPerTick.toDouble()).scale(mediumSpeed.toDouble()))
@@ -110,11 +164,142 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
if (bankDurationTicks == 0) {
banking = false
operation = Operation.WAIT
}
}
// Here we call the wander on every pokemon no matter what then they wander then leave
// Just ignore all operatations and do whatever is being tested.
if (true) {
// val horzOnly = true
//
// var topSpeed = gatherTopSpeed(pokemonEntity)
// var maxForce = 0.02 // Arbitrarily chosen as of right now.
// pokemonEntity.isPokemonSprinting = false
//
// // grab forward travel vector direction
// val forwardVec = if (pokemonEntity.deltaMovement.horizontalDistanceSqr() < 0.001) {
// // No meaningful velocity so derive from yaw orientation instead
// val yaw = Math.toRadians(pokemonEntity.yRot.toDouble())
// Vec3(-sin(yaw), 0.0, cos(yaw))
// } else {
// if (horzOnly) Vec3(pokemonEntity.deltaMovement.x, 0.0, pokemonEntity.deltaMovement.z).normalize()
// else pokemonEntity.deltaMovement.normalize()
// }
// val pointAhead = pokemonEntity.position().add(forwardVec)
// val aheadForce = pokemonEntity.steerToTarget(pointAhead, topSpeed, maxForce, horzOnly = horzOnly)
//
//
//// val player = pokemonEntity.level().getNearestPlayer(pokemonEntity, 10.0) ?: return
//// // Flee direction — away from player, flattened to XZ
//// val toPlayerVec = pokemonEntity.position().subtract(player.position())
////
//// if (toPlayerVec.length() < 6.0) {
//// pokemonEntity.isPokemonSprinting = true
////// topSpeed = 0.4 // hardcoded high value
////// maxForce = 0.04
//// topSpeed *= 2.0f
//// maxForce *= 2
//// }
////
//// val fleePoint = pokemonEntity.position().add(toPlayerVec)
//// val aheadForce = pokemonEntity.steerToTarget(fleePoint, topSpeed, maxForce, horzOnly = horzOnly)
//
// // For now just move forward by seeking towards a point ahead
// val lookAheadTicks = 10 // How far in the future of this entities current movement to look ahead for collisions
// val lookAheadDist = max(pokemonEntity.deltaMovement.length() * lookAheadTicks, pokemonEntity.bbWidth / 2.0) // At least always look ahead the width of the mons own hitbox
//
// // Get the collision box
// val inflationAmount = 0.0 //pokemonEntity.bbWidth / 5.0 //TODO: is this needed? it seems to help a bit keeping them off walls but would stop them from entering gaps they truly could
// val collBox = pokemonEntity.boundingBox.move(forwardVec.scale(lookAheadDist)).inflate(inflationAmount, 0.0, inflationAmount) // Inflate a bit
// val shapes = pokemonEntity.level().getBlockCollisions(pokemonEntity, collBox)
//
// // Gather the nearest shape
// val nearestShape = shapes.minByOrNull { shape ->
// shape.bounds().center.distanceToSqr(pokemonEntity.position())
// }
//
// var avoidancForce = Vec3.ZERO
//
// if (nearestShape != null) {
// var hasLeftForce = false
// var hasRightForce = false
// val right = Vec3(-forwardVec.z, 0.0, forwardVec.x) //TODO: expand to 3 dimensions
//
// val obsCenter = nearestShape.bounds().center
// val toObs = obsCenter.subtract(pokemonEntity.position())
//
// // Get the lateral avoidance point to seek to (the avoidance force really)
// // It is the negated vector of toObs - Proj(toObs->forwardVec)
// val projVec = forwardVec.scale(toObs.dot(forwardVec)) // Can do this because forwardVec is normalized
// val lateralComponent = toObs.subtract(projVec) // TODO: moved back so that two opposing forces turn the dude around
//
// avoidancForce = if (lateralComponent.lengthSqr() < 0.001) { // If its dead ahead then just turn right
// val rightVec = forwardVec.cross(Vec3(0.0,1.0,0.0))
// obsCorrectDir = 1F
// pokemonEntity.steerToTarget(pokemonEntity.position().add(rightVec), topSpeed, maxForce, horzOnly = horzOnly)
// } else {
// // Scale the strength of the avoidance vec based off how close to being dead center it is
//
// var avoidanceVec = lateralComponent.scale(-1.0)
// if (horzOnly) avoidanceVec = Vec3(avoidanceVec.x, 0.0, avoidanceVec.z)
// //var avoidStrength = (abs(toObs.dot(avoidanceVec)) / (pokemonEntity.bbWidth / 2.0) ).coerceIn(0.0, 1.0)
//
// // 0.5 to add half the block width allowance since if it catches the corner of a block it still gets the center
// var avoidStrength = sqrt(1 - (avoidanceVec.length() / (pokemonEntity.bbWidth / 2.0 + 0.5) ).coerceIn(0.0, 0.7))
// // Track which side this force is pushing toward
// //val newObsCorrectDir = if (avoidanceVec.normalize().dot(right) > 0) 1F else -1f
//
// // If the new direction is opposite the last direction then invert it
//// if (obsCorrectDir == 0F) {
//// obsCorrectDir = newObsCorrectDir
//// } else if (newObsCorrectDir != obsCorrectDir) {
//// avoidanceVec = avoidanceVec.scale(-1.0)
//// //avoidStrength = 1.0
//// }
//
// val avoidancePoint = pokemonEntity.position().add(avoidanceVec)
// pokemonEntity.steerToTarget(avoidancePoint, topSpeed, maxForce * avoidStrength, horzOnly = horzOnly)
// }
// } else {
// obsCorrectDir = 0F
// }
//
// // accumulte forces
// // TODO: steertoTarget also checks stuff like this. Should that be different? The forces could compound here
// val accumForce = aheadForce.scale(0.5).add(avoidancForce.scale(1.0))
// var finalForce = if( avoidancForce == Vec3.ZERO) aheadForce
// else avoidancForce
//
// prevForce?.let { finalForce = it.lerp(finalForce, 0.1) }
// prevForce = finalForce
//
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(finalForce)
//
// // Must be called to make sure we have complete control over our entities velocity.
// mob.speed = 0f
// mob.setZza(0f)
// mob.setXxa(0f)
// mob.setDiscardFriction(true)
// pokemonEntity.orientEntityToTravel(1.0f)
// pokemonEntity.setFlying(false)
//
// drawLines.add(EntityDebugLine(forwardVec))
// if (drawLines.isNotEmpty()) {
// sendPacketToTrackingPlayers(pokemonEntity, ClientboundUpdateObsAvoidPacket(pokemonEntity.id, drawLines.toList()))
// drawLines.clear()
// }
if (operation == Operation.STRAFE) {
steerSpeed = gatherTopSpeed(pokemonEntity)
val force = steerArbitrator.calculate(debug = true)
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(force)
mob.speed = 0f
mob.setZza(0f)
mob.setXxa(0f)
mob.setDiscardFriction(true)
pokemonEntity.orientEntityToTravel(1.0f)
} else if (operation == Operation.STRAFE) {
var movingDistanceTotal = Mth.sqrt(strafeForwards * strafeForwards + strafeRight * strafeRight)
if (movingDistanceTotal < 1.0f) {
movingDistanceTotal = 1.0f
@@ -161,10 +346,14 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val horizontalDistanceFromTarget = xDist * xDist + zDist * zDist
val closeHorizontally = horizontalDistanceFromTarget < VERY_CLOSE
if (!closeHorizontally) {
val angleToTarget = Mth.atan2(zDist, xDist).toDegrees() - 90.0f
val currentMovingAngle = mob.yRot
val steppedAngle = Mth.approachDegrees(currentMovingAngle, angleToTarget, 100 * mediumSpeed)
mob.yRot = steppedAngle
// NEW
pokemonEntity.orientEntityToTravel(1.0f)
// OLD
// val angleToTarget = Mth.atan2(zDist, xDist).toDegrees() - 90.0f
// val currentMovingAngle = mob.yRot
// val steppedAngle = Mth.approachDegrees(currentMovingAngle, angleToTarget, 100 * mediumSpeed)
// mob.yRot = steppedAngle
}
val (inWater, inLava) = mob.level().getWaterAndLavaIn(mob.boundingBox)
@@ -191,15 +380,101 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
val scale = min(adjustedSpeed.toDouble(), fullDistance.length())
mob.deltaMovement = direction.scale(scale)
val maxForce = 0.02 // 0.02
// Gather the point to be steered to
val nav = pokemonEntity.navigation
nav.pathFollowTarget?.let { rt ->
wantedX = rt.x
wantedZ = rt.z
wantedY = rt.y
}
// + 0.05 for dealing with swimming out of water, they otherwise get stuck on the lip
val steerForce = pokemonEntity.steerToTarget(Vec3(wantedX, refine(wantedY + 0.05), wantedZ), scale, maxForce)
mob.deltaMovement = mob.deltaMovement.add(steerForce)
//mob.deltaMovement = direction.scale(scale)
xDist = fullDistance.x
zDist = fullDistance.z
yDist = fullDistance.y
} else {
// division is to slow the speed down a bit so they don't overshoot when they get there.
val forwardSpeed = min(adjustedSpeed, max(horizontalDistanceFromTarget.toFloat() / 2, 0.15F))
mob.speed = forwardSpeed
var forwardSpeed = adjustedSpeed
var forceDiv = 1 / 5.0 // Portion of max speed that force can apply every tick
var minForce = 0.02 // If its not at least this it breaks quite a few things. looking at you snorlax
val finalGoal: BlockPos? = pokemonEntity.navigation.path?.target
val distanceToFinalGoal = finalGoal?.let {
pokemonEntity.position().distanceTo(it.center).toFloat()
} ?: 0.0f
if (distanceToFinalGoal > 8f) { //TODO: scale this to hitbox size
pokemonEntity.isPokemonSprinting = true
forwardSpeed *= 1.4f
forceDiv = 1 / 10.0
minForce *= 1.0 //TODO: figure out what to actually do for this
} else {
pokemonEntity.isPokemonSprinting = false
}
// New way
val blockFriction = mob.level()
.getBlockState(mob.getBlockPosBelowThatAffectsMyMovement())
.block.getFriction()
val frictionInfluencedSpeed = if (mob.onGround()) {
forwardSpeed * (0.21600002f / (blockFriction * blockFriction * blockFriction))
} else {
0.02f // getFlyingSpeed() default for non-player controlled. Also hopefully our checks above this caught this case
}
val baseForce = (forwardSpeed * 0.98) * frictionInfluencedSpeed
// We find where the equilibrium between vanilla friction and the input force usually is to get a topSpeed for this mon.
// Could be replaced in the future with direct topSpeed calculations in the pokemon species form
val frictionAppliedAfter = blockFriction * 0.91
val topSpeed = baseForce / (1 - frictionAppliedAfter)
val currSpeed = pokemonEntity.deltaMovement.length()
//TODO: is this needed?
// if (!pokemonEntity.isPokemonSprinting && currSpeed > topSpeed) { // TODO: this might mess up some stuff like tnt pushing mobs around
// pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.normalize().scale(lerp(currSpeed, topSpeed, 0.1))
// }
// Get a force that looks natural
val maxForce = minForce //max(baseForce * forceDiv, minForce)
// Gather the point to be steered to
val nav = pokemonEntity.navigation
nav.pathFollowTarget?.let { rt ->
wantedX = rt.x
wantedZ = rt.z
wantedY = rt.y
}
val steerForce = pokemonEntity.steerToTarget(
Vec3(wantedX, wantedY, wantedZ),
topSpeed, // Is never reached as minecraft relies on top speed being obtained when force=friction
maxForce,
horzOnly = true
)
// If this happens a lot then we have issues
val mdt = LivingEntity.MIN_MOVEMENT_DISTANCE
if ( abs(steerForce.x) < mdt || abs(steerForce.y) < mdt || abs(steerForce.z) < mdt) {
//Cobblemon.LOGGER.info("INFO: Steer Force dimension smaller than minimum movement distance value") //TODO: figure this out
}
pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(steerForce)
//pokemonEntity.deltaMovement = pokemonEntity.deltaMovement.add(Vec3(0.0103766, 0.0, 0.0))
mob.speed = 0f
mob.setZza(0f)
mob.setXxa(0f)
mob.setDiscardFriction(true)
// Old way
// mob.speed = forwardSpeed
}
if (!verticalHandled) {
@@ -280,6 +555,41 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
}
fun gatherTopSpeed( pokemonEntity: PokemonEntity): Double {
val behaviour = pokemonEntity.behaviour
val mediumSpeed = runtime.resolveFloat(if (pokemonEntity.getCurrentPoseType() in setOf(PoseType.FLY, PoseType.HOVER)) {
behaviour.moving.fly.flySpeedHorizontal
} else if (pokemonEntity.isEyeInFluid(FluidTags.WATER) || pokemonEntity.isEyeInFluid(FluidTags.LAVA)) {
behaviour.moving.swim.swimSpeed
} else {
behaviour.moving.walk.walkSpeed
}) * 2.5F //TODO: idk why this is 2.5 but I think we're stuck with it
val baseSpeed = mob.getAttributeValue(Attributes.MOVEMENT_SPEED).toFloat() * 0.4f //TODO: does this ever need to not be hardcoded?
val adjustedSpeed = baseSpeed * mediumSpeed
val forwardSpeed = adjustedSpeed
val blockFriction = mob.level()
.getBlockState(mob.getBlockPosBelowThatAffectsMyMovement())
.block.getFriction()
val frictionInfluencedSpeed = if (mob.onGround()) {
forwardSpeed * (0.21600002f / (blockFriction * blockFriction * blockFriction))
} else {
0.02f // getFlyingSpeed() default for non-player controlled
}
// Force applied every tick through the usual vanilla route
val baseForce = (forwardSpeed * 0.98) * frictionInfluencedSpeed
// We find where the equilibrium between vanilla friction and the input force usually is to get a topSpeed for this mon.
// Could be replaced in the future with direct topSpeed calculations in the pokemon species form
val frictionAppliedAfter = blockFriction * 0.91
val topSpeed = baseForce / (1 - frictionAppliedAfter)
return topSpeed
}
/*
* Returns the ideal water level y position of a given pokemon during battle.
* This is separate for the eye height check and is dependent on if the pokemon is rafted or swimming.
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.entity.pokemon.ai.sensors
import com.cobblemon.mod.common.CobblemonMemories
import com.google.common.collect.ImmutableSet
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.sensing.Sensor
import net.minecraft.world.item.Items
import net.minecraft.world.entity.player.Player
class PlayerDebuggingSensor : Sensor<LivingEntity>(SCAN_INTERVAL) {
override fun doTick(level: ServerLevel, entity: LivingEntity) {
var diamondHolder: LivingEntity? = null
diamondHolder = entity.brain.getMemory(CobblemonMemories.DIAMOND_DEBUGGER).orElse(null)?.let {
if(it.mainHandItem.`is`(Items.DIAMOND)) {
it
} else {
null
}
}
if (diamondHolder == null) {
val nearEntities = entity.brain.getMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES).orElse(mutableListOf())
diamondHolder = nearEntities?.find { entity ->
entity.mainHandItem.`is`(Items.DIAMOND)
}
}
if (diamondHolder != null && diamondHolder is Player) {
entity.brain.setMemory(CobblemonMemories.DIAMOND_DEBUGGER, diamondHolder)
} else {
entity.brain.eraseMemory(CobblemonMemories.DIAMOND_DEBUGGER)
}
}
override fun requires(): Set<MemoryModuleType<*>> {
return setOf(
CobblemonMemories.DIAMOND_DEBUGGER,
MemoryModuleType.NEAREST_LIVING_ENTITIES
)
}
companion object {
const val SCAN_INTERVAL = 10
}
}
@@ -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.entity.pokemon.ai.tasks;
//
//import com.cobblemon.mod.common.CobblemonMemories
//import com.cobblemon.mod.common.api.pokemon.status.Statuses
//import com.cobblemon.mod.common.api.storage.party.PartyStore
//import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
//import net.minecraft.world.entity.ai.behavior.OneShot
//import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder
//import net.minecraft.world.entity.ai.behavior.declarative.Trigger
//import net.minecraft.world.entity.ai.memory.MemoryModuleType
//
//object BoidsTask {
//fun create(): OneShot<PokemonEntity> {
// return BehaviorBuilder.create {
// it.group(
// it.absent(CobblemonMemories.POKEMON_DROWSY)
// ).apply(it) { _ ->
// Trigger { world, entity, _ ->
// if (entity.pokemon.status?.status == Statuses.SLEEP && entity.pokemon.storeCoordinates.get()?.store !is PartyStore) {
// entity.pokemon.status = null
// entity.brain.eraseMemory(CobblemonMemories.POKEMON_SLEEPING)
// entity.brain.useDefaultActivity()
// return@Trigger true
// } else {
// return@Trigger false
// }
// }
// }
// }
//}
//}
@@ -0,0 +1,111 @@
package com.cobblemon.mod.common.entity.pokemon.extensions
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.sign
/*************************************************************************************************************
Steering to Target
Detailed in: https://www.red3d.com/cwr/steer/gdc99/
*************************************************************************************************************/
fun PokemonEntity.steerToTarget(
targetPos: Vec3,
maxSpeed: Double,
maxForce: Double,
arrivalRange: Double? = null,
horzOnly: Boolean = false
): Vec3 {
val cPos = if (horzOnly) Vec3(this.position().x, 0.0, this.position().z) else this.position()
val tPos = if (horzOnly) Vec3(targetPos.x, 0.0, targetPos.z) else targetPos
// Ensure we can't have a negative range here
val absArrivalRange = arrivalRange?.let { abs(arrivalRange) }
// Calculate the vector from the pokemons current position to the
// target position. Set its desired magnitude to max speed or a
// portion of it if within the arrivalRange
val toTargetVec = tPos.subtract(cPos)
val distToTarget = toTargetVec.length()
val desiredSpeed = if (absArrivalRange != null && distToTarget < absArrivalRange) (distToTarget/absArrivalRange) * maxSpeed else maxSpeed
val desiredVec = toTargetVec.normalize().scale(desiredSpeed)
var steer = if (horzOnly) desiredVec.subtract(Vec3(this.deltaMovement.x, 0.0, this.deltaMovement.z))
else desiredVec.subtract(this.deltaMovement)
if (steer.length() > maxForce) {
steer = steer.normalize().scale(maxForce)
}
return steer
}
/*************************************************************************************************************
Steering Target Avoidance
Detailed in: https://www.red3d.com/cwr/steer/gdc99/
*************************************************************************************************************/
fun PokemonEntity.avoidTarget(
targetPos: Vec3,
maxSpeed: Double,
maxForce: Double,
avoidanceRange: Double? = null
): Vec3 {
// Calculate the vector from the targets current position to the
// pokemon position. Set its desired magnitude to max speed scaled
// linearly from 1.0 when right next to the target and 0.0 when on the
// very edge of the avoidance Range
var fromTargetVec = this.position().subtract(targetPos)
// Calculate distance to the target
val distToTarget = fromTargetVec.length()
if (distToTarget < 1.0E-4) {fromTargetVec = Vec3(0.0, 1.0, 0.0)} // Ensure the avoidance vector can't be ZERO
// Return no force otherwise a desired vector will be calculated that slows the velocity.
if (avoidanceRange != null && distToTarget >= avoidanceRange) return Vec3.ZERO
// Scale the avoidance force linearly
val desiredSpeed = when {
avoidanceRange == null -> maxSpeed // If no avoidance range then just always flee as fast as you can
avoidanceRange == 0.0 -> maxSpeed // Avoid weird divide by zero nonsense
distToTarget < avoidanceRange -> (1.0 - (distToTarget / avoidanceRange)) * maxSpeed
else -> 0.0
}
//TODO: determine if this should be scaled by distance. Probably not
val desiredVec = fromTargetVec.normalize() //.scale(desiredSpeed)
// Steer the pokemon towards the desired vector
var steer = desiredVec.subtract(this.deltaMovement)
if (steer.length() > maxForce) {
steer = steer.normalize().scale(maxForce)
}
return steer
}
/*************************************************************************************************************
Lerp the mobs yRot to its direction of travel
*************************************************************************************************************/
fun PokemonEntity.orientEntityToTravel(k: Float = 1.0f) {
val interpAmount = k.coerceIn(0.0f, 1.0f) //TODO: This probably shouldn't be a lerp like this.
if (this.deltaMovement.length() < 0.01) return //LivingEntity.MIN_MOVEMENT_DISTANCE) return
// Ensure the entity is facing the direction it is moving in both pitch and yaw
val normVec = this.deltaMovement.normalize()
val targetAngle = Mth.atan2(normVec.z, normVec.x).toDegrees() - 90.0f
val currAngle = this.yRot
val angleDiff = Mth.wrapDegrees(targetAngle - currAngle)
val maxPerTickRotation = 30.0f // set really high to ignore for now 15.0f // TODO: is something like this needed or should it be a clientside spring or something?
val effRot = min(abs(angleDiff), maxPerTickRotation) * sign(angleDiff)
this.yRot = currAngle + (effRot * interpAmount)
//entity.xRot = asin(normVec.y.toFloat()).toDegrees()
}
@@ -763,7 +763,6 @@ object CobblemonItemGroups {
CobblemonItems.pokeBalls.forEach(entries::accept)
CobblemonItems.pokeRods.forEach(entries::accept)
CobblemonItems.pokedexes.forEach(entries::accept)
entries.accept(CobblemonItems.POKEEDIT_WAND)
}
private fun blocksInjections(injector: Injector) {
@@ -1,48 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.item.interactive
import com.cobblemon.mod.common.client.gui.pokeedit.PokeEditGUI
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.CobblemonItem
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResult
import net.minecraft.world.InteractionResultHolder
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
class PokeEditWandItem : CobblemonItem(Properties().stacksTo(1)) {
override fun use(world: Level, player: Player, usedHand: InteractionHand): InteractionResultHolder<ItemStack> {
val stack = player.getItemInHand(usedHand)
if (!player.isCreative) return InteractionResultHolder.pass(stack)
if (world.isClientSide) {
PokeEditGUI.open()
}
return InteractionResultHolder.sidedSuccess(stack, world.isClientSide)
}
override fun interactLivingEntity(
stack: ItemStack,
player: Player,
entity: LivingEntity,
usedHand: InteractionHand
): InteractionResult {
if (!player.isCreative) return InteractionResult.PASS
if (entity is PokemonEntity) {
if (entity.level().isClientSide) {
PokeEditGUI.open()
}
return InteractionResult.sidedSuccess(entity.level().isClientSide)
}
return InteractionResult.PASS
}
}
@@ -13,11 +13,7 @@ import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.battles.MoveTarget
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeEnumConstant
import com.cobblemon.mod.common.util.writeString
import com.cobblemon.mod.common.util.*
import net.minecraft.network.RegistryFriendlyByteBuf
class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacket<MoveTemplate, MovesRegistrySyncPacket>(moves) {
@@ -37,7 +33,6 @@ class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacke
buffer.writeDouble(entry.critRatio)
buffer.writeInt(entry.effectChances.size)
entry.effectChances.forEach { chance -> buffer.writeDouble(chance) }
buffer.writeFloat(entry.weight)
}
override fun decodeEntry(buffer: RegistryFriendlyByteBuf): MoveTemplate {
@@ -55,21 +50,7 @@ class MovesRegistrySyncPacket(moves: List<MoveTemplate>) : DataRegistrySyncPacke
repeat(buffer.readInt()) {
effectChances += buffer.readDouble()
}
val weight = buffer.readFloat()
return MoveTemplate(
name = name,
num = num,
elementalType = type,
damageCategory = damageCategory,
power = power,
target = target,
accuracy = accuracy,
pp = pp,
priority = priority,
critRatio = critRatio,
effectChances = effectChances.toTypedArray(),
weight = weight
)
return MoveTemplate(name, num, type, damageCategory, power, target, accuracy, pp, priority, critRatio, effectChances.toTypedArray())
}
override fun synchronizeDecoded(entries: Collection<MoveTemplate>) {
@@ -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.net.messages.client.debug
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.render.debug.EntityDebugLine
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeCollection
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.phys.Vec3
/**
* Packet sent to the client to update the obstacle avoidance lines to be rendered
* while debugging
*
* @author Jackson
* @since September 15th, 2025
*/
class ClientboundUpdateObsAvoidPacket(val entityId: Int, val lines: List<EntityDebugLine>) : NetworkPacket<ClientboundUpdateObsAvoidPacket> {
override val id: ResourceLocation = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(entityId)
buffer.writeCollection(lines) { _, linePair ->
linePair.encode(buffer)
}
}
companion object {
val ID = cobblemonResource("obstacle_avoidance_lines")
fun decode(buffer: RegistryFriendlyByteBuf): ClientboundUpdateObsAvoidPacket {
val entityId = buffer.readInt()
val lines = buffer.readList { buf ->
EntityDebugLine.decode(buf as RegistryFriendlyByteBuf)
}
return ClientboundUpdateObsAvoidPacket(entityId, lines)
}
}
}
@@ -1,51 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.pokeedit
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeEnumConstant
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
enum class PokeEditAction {
CREATE,
SPAWN
}
class PokeEditActionPacket(
val action: PokeEditAction,
val properties: String,
val moves: List<String>,
val cosmeticItem: String
) : NetworkPacket<PokeEditActionPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeEnumConstant(action)
buffer.writeString(properties)
buffer.writeInt(moves.size)
moves.forEach(buffer::writeString)
buffer.writeString(cosmeticItem)
}
companion object {
val ID = cobblemonResource("pokeedit_action")
fun decode(buffer: RegistryFriendlyByteBuf): PokeEditActionPacket {
val action = buffer.readEnumConstant(PokeEditAction::class.java)
val properties = buffer.readString()
val moves = List(buffer.readInt()) { buffer.readString() }
val cosmeticItem = buffer.readString()
return PokeEditActionPacket(action, properties, moves, cosmeticItem)
}
}
}
@@ -1,130 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.serverhandling.pokeedit
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.moves.MoveSet
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditAction
import com.cobblemon.mod.common.net.messages.server.pokeedit.PokeEditActionPacket
import com.cobblemon.mod.common.util.commandLang
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.toBlockPos
import net.minecraft.network.chat.Component
import net.minecraft.commands.arguments.item.ItemParser
import com.mojang.brigadier.StringReader
import net.minecraft.world.item.ItemStack
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.MobSpawnType
import net.minecraft.world.level.Level
object PokeEditActionHandler : ServerNetworkPacketHandler<PokeEditActionPacket> {
override fun handle(packet: PokeEditActionPacket, server: MinecraftServer, player: ServerPlayer) {
val properties = PokemonProperties.parse(packet.properties)
if (properties.species == null) {
player.sendSystemMessage(commandLang("spawnpokemon.nospecies").red())
return
}
when (packet.action) {
PokeEditAction.CREATE -> {
if (!Cobblemon.permissionValidator.hasPermission(player, CobblemonPermissions.GIVE_POKEMON_SELF)) {
player.sendSystemMessage(lang("requires_permission").red())
return
}
val pokemon = properties.create(player)
applyMovesIfProvided(pokemon, packet.moves)
applyCosmeticIfProvided(pokemon, packet.cosmeticItem, server, player)
Cobblemon.storage.getParty(player).add(pokemon)
}
PokeEditAction.SPAWN -> {
if (!Cobblemon.permissionValidator.hasPermission(player, CobblemonPermissions.SPAWN_POKEMON)) {
player.sendSystemMessage(lang("requires_permission").red())
return
}
val world = player.serverLevel()
val pos = player.position()
val blockPos = pos.toBlockPos()
if (!Level.isInSpawnableBounds(blockPos)) {
player.sendSystemMessage(Component.literal("Invalid position").red())
return
}
try {
val pokemonEntity = properties.createEntity(world, player)
applyMovesIfProvided(pokemonEntity.pokemon, packet.moves)
applyCosmeticIfProvided(pokemonEntity.pokemon, packet.cosmeticItem, server, player)
pokemonEntity.moveTo(pos.x, pos.y, pos.z, pokemonEntity.yRot, pokemonEntity.xRot)
pokemonEntity.entityData.set(PokemonEntity.SPAWN_DIRECTION, pokemonEntity.random.nextFloat() * 360F)
pokemonEntity.finalizeSpawn(world, world.getCurrentDifficultyAt(blockPos), MobSpawnType.COMMAND, null)
world.addFreshEntity(pokemonEntity)
} catch (e: Exception) {
e.printStackTrace()
player.sendSystemMessage(Component.literal("Unable to spawn at the given position").red())
}
}
}
}
private fun applyMovesIfProvided(pokemon: com.cobblemon.mod.common.pokemon.Pokemon, moves: List<String>) {
if (moves.none { it.isNotBlank() }) return
val templates = moves.take(MoveSet.MOVE_COUNT).map { name ->
if (name.isBlank()) null else Moves.getByName(name)
}
pokemon.moveSet.doWithoutEmitting {
for (i in 0 until MoveSet.MOVE_COUNT) {
pokemon.moveSet.setMove(i, null)
}
templates.forEachIndexed { index, template ->
template?.let { pokemon.moveSet.setMove(index, it.create()) }
}
}
pokemon.moveSet.update()
}
private fun applyCosmeticIfProvided(
pokemon: com.cobblemon.mod.common.pokemon.Pokemon,
cosmeticItem: String,
server: MinecraftServer,
player: ServerPlayer
) {
val trimmed = cosmeticItem.trim()
if (trimmed.isEmpty()) return
val parser = ItemParser(server.registryAccess())
val result = try {
parser.parse(StringReader(trimmed))
} catch (_: Exception) {
player.sendSystemMessage(Component.literal("Invalid cosmetic item: $trimmed").red())
return
}
val stack = ItemStack(result.item)
stack.applyComponents(result.components)
if (stack.isEmpty) return
val isValid = CobblemonCosmeticItems.findValidCosmeticForPokemonAndItem(
server.registryAccess(),
pokemon,
stack
) != null
if (!isValid) {
player.sendSystemMessage(Component.literal("Item is not a valid cosmetic for this pokemon.").red())
return
}
pokemon.swapCosmeticItem(stack, decrement = false)
}
}
@@ -104,10 +104,6 @@ class FormData(
private var _baseAI: MutableList<BehaviourConfig>? = null,
@SerializedName("ai")
private var _ai: MutableList<BehaviourConfig>? = null,
@SerializedName("signatureMoves")
private var _signatureMoves: MutableList<MoveTemplate>? = null,
@SerializedName("defaultWildMovesetBuilder")
private val _defaultWildMovesetBuilder: ResourceLocation? = null,
val requiredMove: String? = null,
val requiredItem: String? = null,
/** For forms that can accept different items (e.g. Arceus-Grass: Meadow Plate or Grassium-Z). */
@@ -239,10 +235,6 @@ class FormData(
get() = _baseAI ?: species.baseAI
val ai: List<BehaviourConfig>
get() = _ai ?: species.ai
val signatureMoves: List<MoveTemplate>
get() = _signatureMoves ?: species.signatureMoves
val defaultWildMovesetBuilder: ResourceLocation
get() = _defaultWildMovesetBuilder ?: species.defaultWildMovesetBuilder
fun eyeHeight(entity: PokemonEntity): Float {
return this.resolveEyeHeight(entity) ?: return this.species.eyeHeight(entity)
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.pokemon
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.CobblemonNetwork.sendPacketToPlayers
import com.cobblemon.mod.common.CobblemonSounds
@@ -35,7 +34,6 @@ import com.cobblemon.mod.common.api.moves.Move
import com.cobblemon.mod.common.api.moves.MoveSet
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.pokeball.PokeBalls
import com.cobblemon.mod.common.api.pokemon.Natures
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
@@ -596,9 +594,6 @@ open class Pokemon : ShowdownIdentifiable {
onChange(MarkingsUpdatePacket({ this }, value))
}
// Last flower fed to a Mooshtank
var lastFlowerFed: ItemStack = ItemStack.EMPTY
fun asRenderablePokemon() = RenderablePokemon(species, aspects, if (heldItemVisible) heldItem else ItemStack.EMPTY)
/**
@@ -1569,8 +1564,10 @@ open class Pokemon : ShowdownIdentifiable {
originalTrainer = fakeTrainerName
}
fun refreshOriginalTrainer() {
when (originalTrainerType) {
fun refreshOriginalTrainer()
{
when (originalTrainerType)
{
OriginalTrainerType.PLAYER -> {
UUID.fromString(originalTrainer)?.let { uuid ->
server()?.profileCache?.get(uuid)?.orElse(null)?.name?.let {
@@ -1587,7 +1584,8 @@ open class Pokemon : ShowdownIdentifiable {
}
}
fun removeOriginalTrainer() {
fun removeOriginalTrainer()
{
originalTrainer = null
originalTrainerType = OriginalTrainerType.NONE
originalTrainerName = null
@@ -1629,11 +1627,14 @@ open class Pokemon : ShowdownIdentifiable {
species = species
checkGender()
if (moveSet.getMoves().isEmpty()) {
initializeMovesetFromDefault()
initializeMoveset()
}
return this
}
// Last flower fed to a Mooshtank
var lastFlowerFed: ItemStack = ItemStack.EMPTY
fun checkGender() {
var reassess = false
if (form.maleRatio !in 0F..1F && gender != Gender.GENDERLESS) {
@@ -1809,20 +1810,6 @@ open class Pokemon : ShowdownIdentifiable {
moveSet.update()
}
fun initializeMovesetFromDefault() {
initializeMovesetFrom(movesetBuilder = CobblemonMovesetBuilders.getOrThrow(form.defaultWildMovesetBuilder))
}
fun initializeMovesetFrom(movesetBuilder: MovesetBuilder) {
val newMoveset = movesetBuilder.build(form = form, level = level)
moveSet.copyFrom(newMoveset)
}
@Deprecated(
message = "Will be removed within potentially 1 title update",
replaceWith = ReplaceWith("initializeMovesetFromDefault"),
level = DeprecationLevel.ERROR
)
fun initializeMoveset(preferLatest: Boolean = true) {
val possibleMoves = form.moves.getLevelUpMovesUpTo(level).toMutableList()
moveSet.doWithoutEmitting {
@@ -22,7 +22,6 @@ import com.cobblemon.mod.common.api.drop.DropTable
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addSpeciesFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.pokemon.effect.ShoulderEffect
@@ -43,7 +42,6 @@ import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
import com.cobblemon.mod.common.pokemon.ai.PokemonBehaviour
import com.cobblemon.mod.common.pokemon.lighthing.LightingData
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEntityDimensions
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readIdentifier
@@ -113,8 +111,6 @@ class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
var implemented = false
var baseAI: MutableList<BehaviourConfig>? = null
var ai = mutableListOf<BehaviourConfig>()
var signatureMoves = mutableListOf<MoveTemplate>()
var defaultWildMovesetBuilder = cobblemonResource("wild")
/**
* The height in decimeters
@@ -77,7 +77,8 @@ object PokemonBrain {
CobblemonSensors.NEARBY_GROWABLE_CROPS,
CobblemonSensors.NEARBY_BEE_HIVE,
CobblemonSensors.NEARBY_FLOWER,
CobblemonSensors.NEARBY_SWEET_BERRY_BUSH
CobblemonSensors.NEARBY_SWEET_BERRY_BUSH,
CobblemonSensors.PLAYER_DEBUGGING
// CobblemonSensors.BATTLING_POKEMON,
// CobblemonSensors.NPC_BATTLING
@@ -179,6 +180,7 @@ object PokemonBrain {
MemoryModuleType.HURT_BY_ENTITY,
MemoryModuleType.NEAREST_PLAYERS,
MemoryModuleType.NEAREST_VISIBLE_PLAYER,
MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER,
MemoryModuleType.ANGRY_AT,
MemoryModuleType.ATTACK_COOLING_DOWN,
CobblemonMemories.POKEMON_DROWSY,
@@ -201,7 +203,8 @@ object PokemonBrain {
CobblemonMemories.HERD_LEADER,
CobblemonMemories.HERD_SIZE,
CobblemonMemories.ATTACK_TARGET_DATA,
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH
CobblemonMemories.NEARBY_SWEET_BERRY_BUSH,
CobblemonMemories.DIAMOND_DEBUGGER
)
private fun coreTasks(pokemon: Pokemon) = buildList<Pair<Int, BehaviorControl<in PokemonEntity>>> {
@@ -87,7 +87,6 @@ object DataKeys {
const val POKEMON_PROPERTIES_MOVES = "Moves"
const val POKEMON_PROPERTIES_HELDITEM = "HeldItem"
const val POKEMON_PROPERTIES_MOVESET_BUILDERS = "MovesetBuilders"
// Entity effects
const val ENTITY_EFFECTS = "EntityEffects"
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molang.ExpressionLike
/**
* An IntRange where the minimum and maximum values are defined by MoLang expressions. When deserializing from
* JSON the [com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter] will figure out if it's a single
* value in which case it's not using any scripting stuff. Otherwise, if it's an object with "min" and "max" properties
* it will respect them as expressions.
*
* @author Hiroku
* @since December 15th, 2025
*/
class ScriptableIntRange(
val min: ExpressionLike = "0".asExpressionLike(),
val max: ExpressionLike = "0".asExpressionLike()
) {
constructor(min: Int, max: Int): this(
min = min.toString().asExpressionLike(),
max = max.toString().asExpressionLike()
)
fun resolve(runtime: MoLangRuntime): IntRange {
val minimum = min.resolveInt(runtime)
val maximum = max.resolveInt(runtime)
if (minimum > maximum) {
Cobblemon.LOGGER.error("ScriptableIntRange(${min.getString()}, ${max.getString()}) resolved a minimum ($minimum) greater than maximum ($maximum). Returning empty range.")
return IntRange.EMPTY
}
return minimum..maximum
}
}
@@ -1,26 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
object MovesetBuilderAdapter : JsonDeserializer<MovesetBuilder> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MovesetBuilder {
val jsonObject = json.asJsonObject
val type = jsonObject.get("type")?.asString ?: "cobblemon:default"
val clazz = MovesetBuilder.movesetBuilderTypes[type.asIdentifierDefaultingNamespace()]
?: throw IllegalArgumentException("Unknown MovesetBuilder type: $type")
return context.deserialize(json, clazz)
}
}
@@ -1,23 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
object MovesetBuilderReferenceAdapter : JsonDeserializer<MovesetBuilder> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MovesetBuilder {
return CobblemonMovesetBuilders.getOrThrow(json.asString.asIdentifierDefaultingNamespace())
}
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
@@ -17,8 +16,8 @@ import java.lang.reflect.Type
object NPCPartyProviderAdapter : JsonDeserializer<NPCPartyProvider> {
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): NPCPartyProvider {
val typeName = (if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString).asIdentifierDefaultingNamespace()
return NPCPartyProvider.types[typeName]?.invoke()?.also { it.loadFromJSON(json) }
val typeName = if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString
return NPCPartyProvider.types[typeName]?.invoke(typeName)?.also { it.loadFromJSON(json) }
?: throw IllegalStateException("Unable to find party provider by type: $typeName")
}
}
@@ -1,53 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.asExpressionLike
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import java.lang.reflect.Type
/**
* Parses a [ScriptableIntRange] by two paths. One is if it's a primitive in which
* case they're just doing a normal [IntRange] and it will parse it using the same
* method as IntRange normally gets and then returns a ScriptableIntRange of literals.
*
* The other path is if they provide an object with "min" and "max" in which case
* each property is deserialized as an [ExpressionLike].
*
* @author Hiroku
* @since December 15th, 2025
*/
object ScriptableIntRangeAdapter : JsonDeserializer<ScriptableIntRange> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): ScriptableIntRange {
if (json is JsonPrimitive) {
IntRangeAdapter.deserialize(json, typeOfT, context).let {
return ScriptableIntRange(
it.first.toString().asExpressionLike(),
it.last.toString().asExpressionLike()
)
}
} else {
val obj = json.asJsonObject
val min = obj.get("min").asString
val max = obj.get("max").asString
return ScriptableIntRange(
min.toString().asExpressionLike(),
max.toString().asExpressionLike()
)
}
}
}
@@ -1,29 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.util.adapters
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.lang.reflect.Type
/**
* A simple adapter that maps string values to objects based on a provided mapping.
*
* I could swear I already made something like this before.
*
* @author Hiroku
* @since December 5th, 2025
*/
class StringToObjectAdapter<T>(
val mapping: MutableMap<String, T>
) : JsonDeserializer<T> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T = mapping[json.asString]
?: throw IllegalArgumentException("Unknown mapping for string: ${json.asString}")
}
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more