Compare commits

..
Author SHA1 Message Date
Landon Wills 5cb1cdceb6 Use opponents model and skin for remote showdown battles (needs cleanup) 2025-03-27 20:03:39 -04:00
Landon Wills 603db456b0 Fail gracefully if errors occur on connect or disconnect from remote showdown services 2025-03-26 22:52:20 -04:00
Landon Wills 6fb19ccd0b Revise logic to fetch showdown logs to be more consistent 2025-03-26 22:31:00 -04:00
Landon Wills aebe2bb2d9 Prototype of wifi battles 2025-03-25 22:44:13 -04:00
Landon Wills c11f2b15d9 Support battle processing on other showdown services 2025-03-25 19:41:31 -04:00
Landon Wills 50eefd5307 Implement base of remote showdown service 2025-03-25 00:06:37 -04:00
Landon Wills 9fa3cd86af Make action buttons work in debug ui 2025-03-24 22:55:51 -04:00
Landon Wills 27494bb654 Merge branch 'refs/heads/main' into showdown-fun 2025-03-24 00:17:56 -04:00
Hiroku 2413981f6f Merge branch 'charizardmarketing' into 'main'
Updated Charizard with the latest model and animations to be used for marketing on the main branch.

See merge request cable-mc/cobblemon!1572
2025-03-24 01:23:40 +00:00
Tyzillion 4ac244003a -6+ ?????? 2025-03-23 21:22:47 -04:00
Tyzillion 9ac92a084f Updated Charizard with new locators. 2025-03-23 21:18:31 -04:00
Apion aacbcb1f28 Merge branch 'jackowes/concrete_bird' into 'main'
Overhaul of bird and fix of jet velocity bug

See merge request cable-mc/cobblemon!1563
2025-03-23 23:37:47 +00:00
Jackson BowesandApion 39ff7e418c Overhaul of bird and fix of jet velocity bug 2025-03-23 23:37:47 +00:00
Hiroku 58498a091d Merge branch 'josh/npc-item-rendering' into 'main'
Molang for rendering items during animations.

See merge request cable-mc/cobblemon!1570
2025-03-23 23:12:06 +00:00
Hiroku a806f77aff Merge branch 'entity-scale' into 'main'
Pokemon hitboxes scale with attribute generic.scale

Closes #1410

See merge request cable-mc/cobblemon!1529
2025-03-23 22:05:38 +00:00
JordanandHiroku 60ae9e3a55 Pokemon hitboxes scale with attribute generic.scale 2025-03-23 22:05:37 +00:00
Hiroku 03ddc35d36 Merge branch 'Fix-pokemon-loadFrom' into 'main'
Fixed Pokemon.loadFrom[JSON/NBT]

Closes #1507

See merge request cable-mc/cobblemon!1571
2025-03-23 03:22:17 +00:00
ShiroFenixandHiroku 1b8e9b9b36 Fixed Pokemon.loadFrom[JSON/NBT] 2025-03-23 03:22:17 +00:00
Paul e52ac956f0 Added q.entity.is_holding_item for pokemon posers to use 2025-03-23 12:40:59 +10:00
Josh 4bfe147730 Merge branch 'main' into josh/npc-item-rendering 2025-03-22 12:41:31 -04:00
Landon Wills 3aa7e72839 Add ability to cycle between showdown services to debug UI 2025-03-22 02:21:31 -04:00
Landon Wills 4be4da58c0 Add support for multiple showdown connections, more work on showdown debugging UI 2025-03-22 01:20:16 -04:00
Landon Wills 5b60a822ef Remove broken socket service 2025-03-21 22:31:46 -04:00
Landon Wills 7457827f27 Start refactoring showdown architecture to support several services 2025-03-21 22:30:48 -04:00
Hiroku 493664bef2 Merge branch 'mealam/pokeballlocation' into 'main'
fix: Adjust `NPCEntity` pokeball throw positioning to account for `baseScale`

See merge request cable-mc/cobblemon!1568
2025-03-21 23:50:33 +00:00
AramandHiroku 981e3e5efc fix: Adjust NPCEntity pokeball throw positioning to account for baseScale 2025-03-21 23:50:33 +00:00
Josh 963614f5fa Updated CHANGELOG.md 2025-03-21 11:51:18 -04:00
Josh 7fdb70e3c3 Updated trainer npcs for testing 2025-03-21 11:20:24 -04:00
Landon Wills 39030fa18d Create base of showdown debugging GUI 2025-03-21 02:09:07 -04:00
Landon Wills 01f720e274 Add utility to allow us to get and post http requests 2025-03-20 22:41:54 -04:00
Josh 37e8f0eab6 multiple items can render on a model per locator and the render_item() molang function now supports items with components 2025-03-20 16:14:16 -04:00
Josh 05edafa5c4 NPC Trainers send out/return animation are able to show the correct ball. 2025-03-19 22:35:00 -04:00
Hiroku c15350e647 Merge branch 'mealam/1499' into 'main'
fix: Adjust `NPCEntity` beam positioning to account for `baseScale`

Closes #1499

See merge request cable-mc/cobblemon!1567
2025-03-20 00:36:12 +00:00
AramandHiroku f9e5784b10 fix: Adjust NPCEntity beam positioning to account for baseScale 2025-03-20 00:36:12 +00:00
Josh cea600617e Added render_item() molang function for PosableState. Allows rendering dummy items for animations. 2025-03-19 18:47:11 -04:00
66 changed files with 2632 additions and 952 deletions
+5
View File
@@ -17,6 +17,7 @@
- Substantially optimised spawning checks mainly by front-loading biome filtering.
- When using the `cobblemon` or `generation_9` capture calculators a critical capture with a single shake will always play for successful captures when you've already registered the Pokémon as caught in your Pokédex.
- Improved the performance of saving Pokédex and player data.
- Pokémon hitbox now scales with entity attribute `generic.scale`.
### Fixes
- Fixed Particles sometimes facing the wrong direction (looking at you, Swords Dance)
@@ -58,6 +59,9 @@
- Note: This will break mods that used our observable functionality there or in MoveSet, IVs, EVs, or BenchedMoves.
- Using `Pokemon#onChange()` is now the way to mark a Pokémon as needing a save.
- Using `[Pokemon].changeObservable` is now the way to get an `Observable` for any save-worthy changes.
- Updated NPCEntity beam positioning to properly account for the baseScale property.
- Updated NPCEntity pokeball throw positioning to properly account for the baseScale property.
- Fixed `[Pokemon].copyFrom` error causing forms, IVs, and EVs to not be applied properly when using `[Pokemon].loadFromJSON` or `[Pokemon].loadFromNBT`
### MoLang & Datapacks
- The following usages for item predicates can now use item conditions like advancements do, you can learn about them in the [Minecraft wiki](https://minecraft.wiki/w/Advancement_definition#minecraft:filled_bucket)
@@ -68,6 +72,7 @@
- Added `interpolate` boolean property to animated textures to allow gradual colour changes between frames.
- Fixed species additions not being capable of changing implemented status.
- Added support for action effects that are triggered by `|-activate|` Showdown instructions. `activate_{effect_id}` is the syntax.
- Added Molang functions for rendering items `render_item(item_id, locator_name)` and `clear_items()`.
## [1.6.1 (January 26th, 2025)](#1-6-1)
@@ -25,7 +25,6 @@ object CobblemonCommands {
ChangeWalkSpeed.register(dispatcher)
ChangeJointScaleCommand.register(dispatcher)
TestCommand.register(dispatcher)
ReloadShowdownCommand.register(dispatcher)
ClickTextCommand.register(dispatcher)
PokemonEditCommand.register(dispatcher)
TeachCommand.register(dispatcher)
@@ -62,11 +61,10 @@ object CobblemonCommands {
RunMolangScriptCommand.register(dispatcher)
CobblemonConfigCommand.register(dispatcher)
UnlockPCBoxWallpaperCommand.register(dispatcher)
AlterShinyChanceCommand.register(dispatcher)
// Possibly lock down registration if and only if under dev environment or running in an environment
// with a certain system environment variable set
CobblemonInfoCommand.register(dispatcher)
CalculateSeatPositionCommand.register(dispatcher)
}
}
}
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.client.net.callback.party.OpenPartyCallbackHandl
import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallbackHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.UnlockReloadPacketHandler
import com.cobblemon.mod.common.client.net.debug.showdown.ShowdownRefreshInfoHandler
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.RunPosableMoLangHandler
@@ -48,14 +49,7 @@ import com.cobblemon.mod.common.client.net.storage.party.SetPartyPokemonHandler
import com.cobblemon.mod.common.client.net.storage.party.SetPartyReferenceHandler
import com.cobblemon.mod.common.client.net.storage.pc.*
import com.cobblemon.mod.common.client.net.toast.ToastPacketHandler
import com.cobblemon.mod.common.client.net.trade.TradeAcceptanceChangedHandler
import com.cobblemon.mod.common.client.net.trade.TradeCancelledHandler
import com.cobblemon.mod.common.client.net.trade.TradeCompletedHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferExpiredHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferNotificationHandler
import com.cobblemon.mod.common.client.net.trade.TradeProcessStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeUpdatedHandler
import com.cobblemon.mod.common.client.net.trade.*
import com.cobblemon.mod.common.net.PacketRegisterInfo
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import com.cobblemon.mod.common.net.messages.client.PlayerInteractOptionsPacket
@@ -66,6 +60,7 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenMoveCallbackPac
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyCallbackPacket
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallbackPacket
import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
@@ -96,42 +91,18 @@ import com.cobblemon.mod.common.net.messages.client.storage.party.InitializePart
import com.cobblemon.mod.common.net.messages.client.storage.party.MoveClientPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyReferencePacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.ClosePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.InitializePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.MoveClientPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.OpenPCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.RenamePCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.*
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.ChangePCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.RequestPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.SetPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.UnlockPCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeAcceptanceChangedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCancelledPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCompletedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferExpiredPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferNotificationPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeProcessStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeUpdatedPacket
import com.cobblemon.mod.common.net.messages.client.trade.*
import com.cobblemon.mod.common.net.messages.client.ui.InteractPokemonUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.PokedexUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.SummaryUIPacket
import com.cobblemon.mod.common.net.messages.server.BattleChallengePacket
import com.cobblemon.mod.common.net.messages.server.BattleChallengeResponsePacket
import com.cobblemon.mod.common.net.messages.server.BenchMovePacket
import com.cobblemon.mod.common.net.messages.server.RequestMoveSwapPacket
import com.cobblemon.mod.common.net.messages.server.RequestPlayerInteractionsPacket
import com.cobblemon.mod.common.net.messages.server.SelectStarterPacket
import com.cobblemon.mod.common.net.messages.server.SendOutPokemonPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamRequestPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamResponsePacket
import com.cobblemon.mod.common.net.messages.server.battle.RemoveSpectatorPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.net.messages.server.*
import com.cobblemon.mod.common.net.messages.server.battle.*
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectedPacket
@@ -139,6 +110,9 @@ import com.cobblemon.mod.common.net.messages.server.callback.party.PartyPokemonS
import com.cobblemon.mod.common.net.messages.server.callback.party.PartySelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyMoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyPokemonMoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.RequestShowdownInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownActionPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownInputPacket
import com.cobblemon.mod.common.net.messages.server.dialogue.EscapeDialoguePacket
import com.cobblemon.mod.common.net.messages.server.dialogue.InputToDialoguePacket
import com.cobblemon.mod.common.net.messages.server.npc.SaveNPCPacket
@@ -158,21 +132,11 @@ import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokem
import com.cobblemon.mod.common.net.messages.server.storage.party.ReleasePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.SwapPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.pc.*
import com.cobblemon.mod.common.net.messages.server.trade.AcceptTradeRequestPacket
import com.cobblemon.mod.common.net.messages.server.trade.CancelTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.ChangeTradeAcceptancePacket
import com.cobblemon.mod.common.net.messages.server.trade.OfferTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.PerformTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.UpdateTradeOfferPacket
import com.cobblemon.mod.common.net.messages.server.trade.*
import com.cobblemon.mod.common.net.serverhandling.ChallengeHandler
import com.cobblemon.mod.common.net.serverhandling.ChallengeResponseHandler
import com.cobblemon.mod.common.net.serverhandling.RequestInteractionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.BattleSelectActionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.RemoveSpectatorHandler
import com.cobblemon.mod.common.net.serverhandling.battle.SpectateBattleHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamLeaveHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestResponseHandler
import com.cobblemon.mod.common.net.serverhandling.battle.*
import com.cobblemon.mod.common.net.serverhandling.block.AdjustBlockEntityViewerCountHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectedHandler
@@ -180,6 +144,9 @@ import com.cobblemon.mod.common.net.serverhandling.callback.party.PartyPokemonSe
import com.cobblemon.mod.common.net.serverhandling.callback.party.PartySelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyMoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyPokemonMoveSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.debug.showdown.RequestShowdownInfoHandler
import com.cobblemon.mod.common.net.serverhandling.debug.showdown.ShowdownActionHandler
import com.cobblemon.mod.common.net.serverhandling.debug.showdown.ShowdownInputHandler
import com.cobblemon.mod.common.net.serverhandling.dialogue.EscapeDialogueHandler
import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandler
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
@@ -203,12 +170,7 @@ import com.cobblemon.mod.common.net.serverhandling.storage.party.MovePartyPokemo
import com.cobblemon.mod.common.net.serverhandling.storage.party.ReleasePCPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.SwapPartyPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.pc.*
import com.cobblemon.mod.common.net.serverhandling.trade.AcceptTradeRequestHandler
import com.cobblemon.mod.common.net.serverhandling.trade.CancelTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.ChangeTradeAcceptanceHandler
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.net.serverhandling.trade.*
import com.cobblemon.mod.common.util.server
import net.minecraft.server.level.ServerPlayer
@@ -412,6 +374,7 @@ object CobblemonNetwork {
// Debug / cheats
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
list.add(PacketRegisterInfo(ShowdownRefreshInfoPacket.ID, ShowdownRefreshInfoPacket::decode, ShowdownRefreshInfoHandler))
return list
}
@@ -511,6 +474,11 @@ object CobblemonNetwork {
// Orientation packet(s)
list.add(PacketRegisterInfo(C2SUpdateOrientationPacket.ID, C2SUpdateOrientationPacket::decode, OrientationPacketHandler))
// Debug / cheats
list.add(PacketRegisterInfo(RequestShowdownInfoPacket.ID, RequestShowdownInfoPacket::decode, RequestShowdownInfoHandler))
list.add(PacketRegisterInfo(ShowdownInputPacket.ID, ShowdownInputPacket::decode, ShowdownInputHandler))
list.add(PacketRegisterInfo(ShowdownActionPacket.ID, ShowdownActionPacket::decode, ShowdownActionHandler))
return list
}
@@ -37,7 +37,7 @@ object Abilities : DataRegistry {
PotentialAbility.types.add(CommonAbilityType)
PotentialAbility.types.add(HiddenAbilityType)
this.abilityMap.clear()
val abilitiesJson = ShowdownService.service.getAbilityIds()
val abilitiesJson = ShowdownService.get().getAbilityIds()
for (i in 0 until abilitiesJson.size()) {
val id = abilitiesJson[i].asString
val ability = AbilityTemplate(id)
@@ -67,4 +67,4 @@ object Abilities : DataRegistry {
abilities.forEach { ability -> this.register(ability) }
}
}
}
@@ -15,10 +15,7 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
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.battles.model.actor.*
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
@@ -74,7 +71,9 @@ import net.minecraft.world.item.ItemStack
open class PokemonBattle(
val format: BattleFormat,
val side1: BattleSide,
val side2: BattleSide
val side2: BattleSide,
val service: ShowdownService = ShowdownService.get(),
val battleId: UUID = UUID.randomUUID()
) {
/** Whether logging will be silenced for this battle. */
var mute = true
@@ -109,8 +108,6 @@ open class PokemonBattle(
val players = playerUUIDs.mapNotNull { it.getPlayer() }
val spectators = mutableSetOf<UUID>()
val battleId = UUID.randomUUID()
val showdownMessages = mutableListOf<String>()
val battleLog = mutableListOf<String>()
val chatLog = mutableListOf<Component>()
@@ -225,7 +222,7 @@ open class PokemonBattle(
fun writeShowdownAction(vararg messages: String) {
log(messages.joinToString("\n"))
ShowdownService.service.send(battleId, messages.toList().toTypedArray())
service.send(battleId, messages.toList().toTypedArray())
}
fun turn(newTurnNumber: Int) {
@@ -378,7 +375,6 @@ open class PokemonBattle(
fun dispatch(dispatcher: () -> DispatchResult) {
dispatches.add(BattleDispatch { dispatcher() })
}
fun dispatchToFront(dispatcher: () -> DispatchResult) {
@@ -502,6 +498,7 @@ open class PokemonBattle(
}
fun checkForInputDispatch() {
actors.filter { it is UnionBattleActor }.forEach { it.mustChoose = false }
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
if (readyToInput && captureActions.isEmpty()) {
@@ -558,4 +555,4 @@ open class PokemonBattle(
queryStruct.addFunction("has_rule") { params -> DoubleValue(params.getString(0) in format.ruleSet) }
return queryStruct
}
}
}
@@ -42,4 +42,4 @@ abstract class AIBattleActor(
setActionResponses(request!!.iterate(this.activePokemon) { _, _, _ -> PassActionResponse })
}
}
}
}
@@ -73,7 +73,7 @@ abstract class BattleActor(
open fun isForPlayer(serverPlayerEntity: ServerPlayer) = serverPlayerEntity.uuid in getPlayerUUIDs()
open fun isForPokemon(pokemonEntity: PokemonEntity) = activePokemon.any { it.battlePokemon?.effectedPokemon?.entity == pokemonEntity }
fun turn() {
open fun turn() {
val request = request ?: return
responses.clear()
if(activePokemon.any { it.isAlive() }) {
@@ -90,7 +90,7 @@ abstract class BattleActor(
}
}
fun upkeep() {
open fun upkeep() {
val request = request ?: return
val forceSwitchPokemon = request.forceSwitch.mapIndexedNotNull { index, b -> if (b) activePokemon[index] else null }
if (forceSwitchPokemon.isEmpty()) {
@@ -125,7 +125,7 @@ abstract class BattleActor(
battle.checkForInputDispatch()
}
fun writeShowdownResponse() {
open fun writeShowdownResponse() {
val showdownMessages = mutableListOf<String>()
var index = 0
request!!.iterate(activePokemon) { activeBattlePokemon, showdownMoveSet, forceSwitch ->
@@ -159,4 +159,4 @@ abstract class BattleActor(
open fun win(otherWinners: List<BattleActor>, losers: List<BattleActor>) {}
open fun lose(winners: List<BattleActor>, otherLosers: List<BattleActor>) {}
}
}
@@ -0,0 +1,25 @@
package com.cobblemon.mod.common.api.battles.model.actor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
class UnionBattleActor(npc: NPCEntity, pokemonList: List<BattlePokemon>) : NPCBattleActor(npc, pokemonList.toMutableList(), 1) {
override fun writeShowdownResponse() {}
override fun onChoiceRequested() {
this.mustChoose = false
}
override fun turn() {
super.turn()
this.mustChoose = false
}
override fun upkeep() {
super.upkeep()
this.mustChoose = false
}
}
@@ -71,6 +71,8 @@ import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.pokemon.evolution.variants.ItemInteractionEvolution
import com.cobblemon.mod.common.pokemon.evolution.variants.LevelUpEvolution
import com.cobblemon.mod.common.pokemon.evolution.variants.TradeEvolution
import com.cobblemon.mod.common.pokemon.riding.controllers.BirdAirController
import com.cobblemon.mod.common.pokemon.riding.states.BirdAirState
import com.cobblemon.mod.common.util.*
import com.mojang.datafixers.util.Either
import java.util.UUID
@@ -786,8 +788,10 @@ object MoLangFunctions {
map.put("in_battle") { DoubleValue(pokemonEntity.isBattling) }
map.put("is_moving") { DoubleValue((pokemonEntity.moveControl as? PokemonMoveControl)?.hasWanted() == true) }
map.put("is_flying") { DoubleValue(pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING)) }
map.put("is_gliding") { DoubleValue(pokemonEntity.useRidingAltPose()) }
map.put("has_aspect") { DoubleValue(it.getString(0) in pokemonEntity.aspects) }
map.put("is_pokemon") { DoubleValue.ONE }
map.put("is_holding_item") { DoubleValue(!pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).isEmpty) }
map
}
)
@@ -40,7 +40,7 @@ object Moves : DataRegistry {
override fun reload(manager: ResourceManager) {
this.allMoves.clear()
this.idMapping.clear()
val movesJson = ShowdownService.service.getMoves()
val movesJson = ShowdownService.get().getMoves()
for (i in 0 until movesJson.size()) {
val jsMove = movesJson[i].asJsonObject
val id = jsMove.get("id").asString
@@ -116,4 +116,4 @@ object Moves : DataRegistry {
this.idMapping[move.num] = move
}
}
}
@@ -13,9 +13,6 @@ object CobblemonPermissions {
private const val COMMAND_PREFIX = "command."
private val permissions = arrayListOf<Permission>()
@JvmStatic
val ALTER_SHINY_CHANCE = this.create("${COMMAND_PREFIX}altershinychance", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val CHANGE_EYE_HEIGHT = this.create("${COMMAND_PREFIX}changeeyeheight", PermissionLevel.ALL_COMMANDS)
@JvmStatic
@@ -134,6 +134,11 @@ data class RidingManager(val entity: PokemonEntity) {
return controller.jumpForce(entity, driver, jumpStrength)
}
fun useRidingAltPose(entity: PokemonEntity, driver: Player): Boolean {
val controller = getController(entity) ?: return false
return controller.useRidingAltPose(entity, driver)
}
/**
* Used to supply the delta for the lerp() between velocity
* vectors. Called inertia for now as the way it feels when
@@ -111,6 +111,8 @@ interface RideController : Encodable, Decodable {
fun useAngVelSmoothing(entity: PokemonEntity): Boolean = false
fun useRidingAltPose(entity: PokemonEntity, driver: Player): Boolean = false
//If function is not overwritten by controllers then just perform the defualt
//rolling function which is roll on mousex and pitch on mousey
fun rotationOnMouseXY(
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
@@ -59,7 +60,9 @@ object BattleBuilder {
battleFormat: BattleFormat = BattleFormat.GEN_9_SINGLES,
cloneParties: Boolean = false,
healFirst: Boolean = false,
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() }
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() },
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val adjustLevel = battleFormat.adjustLevel
@@ -116,7 +119,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(player1Actor),
side2 = BattleSide(player2Actor)
side2 = BattleSide(player2Actor),
service = service,
battleId = battleId
).ifSuccessful {
it.battlePartyStores.addAll(battlePartyStores)
}
@@ -132,7 +137,9 @@ object BattleBuilder {
battleFormat: BattleFormat = BattleFormat.GEN_9_MULTI,
cloneParties: Boolean = false,
healFirst: Boolean = false,
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() }
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() },
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val adjustLevel = battleFormat.adjustLevel
val teams = players.mapIndexed { index, it ->
@@ -213,7 +220,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(playerActors[0], playerActors[1]),
side2 = BattleSide(playerActors[2], playerActors[3])
side2 = BattleSide(playerActors[2], playerActors[3]),
service = service,
battleId = battleId
).ifSuccessful {
it.battlePartyStores.addAll(battlePartyStores)
}
@@ -245,7 +254,9 @@ object BattleBuilder {
cloneParties: Boolean = false,
healFirst: Boolean = false,
fleeDistance: Float = Cobblemon.config.defaultFleeDistance,
party: PartyStore = player.party()
party: PartyStore = player.party(),
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val playerTeam = party.toBattleTeam(clone = cloneParties, healPokemon = healFirst, leadingPokemon = leadingPokemon).sortedBy { it.health <= 0 }
val playerActor = PlayerBattleActor(player.uuid, playerTeam)
@@ -286,7 +297,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(playerActor),
side2 = BattleSide(wildActor)
side2 = BattleSide(wildActor),
service = service,
battleId = battleId
).ifSuccessful {
if (!cloneParties) {
pokemonEntity.battleId = it.battleId
@@ -316,7 +329,9 @@ object BattleBuilder {
battleFormat: BattleFormat = BattleFormat.GEN_9_SINGLES,
cloneParties: Boolean = false,
healFirst: Boolean = false,
party: PartyStore = player.party()
party: PartyStore = player.party(),
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val playerTeam = party.toBattleTeam(clone = cloneParties, healPokemon = healFirst, leadingPokemon = leadingPokemon)
val playerActor = PlayerBattleActor(player.uuid, playerTeam)
@@ -363,7 +378,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(playerActor),
side2 = BattleSide(npcActor)
side2 = BattleSide(npcActor),
service = service,
battleId = battleId
).ifSuccessful { battle ->
npcEntity.entityData.update(NPCEntity.BATTLE_IDS) { it + battle.battleId }
}
@@ -537,4 +554,4 @@ open class ErroredBattleStart(
val errors: Iterable<BattleStartError>
get() = generalErrors + participantErrors.flatMap { it.value }
}
}
@@ -125,7 +125,7 @@ object BattleRegistry {
return team.joinToString("]")
}
private fun startShowdown(battle: PokemonBattle) {
private fun startShowdown(battle: PokemonBattle, sendMessages: Boolean = true) {
// Build request message
val messages = mutableListOf<String>()
@@ -176,7 +176,9 @@ object BattleRegistry {
}
// Compiles the request and sends it off
ShowdownService.service.startBattle(battle, messages.toTypedArray())
if (sendMessages) {
battle.service.startBattle(battle, messages.toTypedArray())
}
}
/**
@@ -186,6 +188,7 @@ object BattleRegistry {
* @param side1 The first side for the battle.
* @param side2 The second side for the battle.
* @param canPreempt Whether the battle can be blocked by subscribers to [CobblemonEvents.BATTLE_STARTED_PRE].
* @param service The showdown service to use for the battle.
*
* NOTE: this does NOT validate the [battleFormat] or sides before launching Showdown.
* This creates the battle and hands it off to Showdown - YOU WILL GET ISSUES IF THE BATTLE IS INVALID!
@@ -195,12 +198,15 @@ object BattleRegistry {
battleFormat: BattleFormat,
side1: BattleSide,
side2: BattleSide,
canPreempt: Boolean = true
canPreempt: Boolean = true,
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID(),
sendMessages: Boolean = true
): BattleStartResult {
val battle = PokemonBattle(battleFormat, side1, side2)
val battle = PokemonBattle(battleFormat, side1, side2, service, battleId)
val start: () -> Unit = {
battleMap[battle.battleId] = battle
startShowdown(battle)
startShowdown(battle, sendMessages)
}
if (!canPreempt) start().also { return SuccessfulBattleStart(battle) }
@@ -234,4 +240,4 @@ object BattleRegistry {
fun tick() {
battleMap.forEachValue(Long.MAX_VALUE) { it.tick() }
}
}
}
@@ -23,13 +23,13 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
this.start()
this.latch.await()
for (action in whenReady) {
action(ShowdownService.service)
action(ShowdownService.get())
}
}
fun queue(action: (ShowdownService) -> Unit) {
if (this.latch.count == 0L) {
action(ShowdownService.service)
action(ShowdownService.get())
} else {
this.whenReady.add(action)
}
@@ -37,8 +37,8 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
override fun run() {
LOGGER.info("Starting showdown service...")
ShowdownService.service.openConnection()
ShowdownService.get().openConnection()
LOGGER.info("Showdown has been started!")
this.latch.countDown()
}
}
}
@@ -49,4 +49,4 @@ class RequestInstruction(val battleActor: BattleActor, val message: BattleMessag
}
}
}
}
@@ -0,0 +1,64 @@
package com.cobblemon.mod.common.battles.runner
import com.cobblemon.mod.common.Cobblemon
import java.text.SimpleDateFormat
import java.util.Date
/**
* Class responsible for logging debug messages for the showdown service.
*
* @author landonjw
*/
class ShowdownDebugLog {
val log = mutableListOf<LogMessage>()
/**
* Adds a log message to the debug log.
*
* @param source The source of the log message.
* @param message The log message.
*/
fun add(source: String, message: String) {
if (!Cobblemon.config.enableDebugKeys) return
val now = Date()
val timestamp = SimpleDateFormat("HH:mm:ss").format(now)
log.add(LogMessage("[$source][${timestamp}]> $message", now))
}
/**
* Clears all log messages from the debug log.
*/
fun clear() {
log.clear()
}
/**
* Retrieves log messages after a specified date.
*
* @param after The date after which to retrieve log messages. If null, retrieves the last 250 messages.
* @return A list of log messages.
*/
fun get(after: Date?): List<String> {
if (after == null) return log.take(250).map { it.message }
return log.filter { it.time.after(after) }.map { it.message }
}
/**
* Checks if the provided messages are the start of the log.
*
* @param messages The messages to check.
* @return True if the messages are the start of the log, false otherwise.
*/
fun isStartOfLog(messages: List<String>): Boolean {
if (messages.isEmpty() && log.isEmpty()) return true
return messages.size == log.size && messages.first() == log.first().message
}
/**
* Data class representing a log message.
*
* @property message The log message.
* @property time The time the log message was created.
*/
data class LogMessage(val message: String, val time: Date)
}
@@ -10,32 +10,154 @@ package com.cobblemon.mod.common.battles.runner
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.runner.graal.GraalShowdownService
import com.cobblemon.mod.common.battles.runner.remote.UnionShowdownService
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.JsonArray
import net.minecraft.resources.ResourceLocation
import java.util.UUID
/**
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service.
*
* All outgoing calls to showdown should be done through this layer to decouple the mod from a specific implementation.
* To get an instance of the service, use the singleton provided via `ShowdownService.get()`
* To get an instance of the service, use the singleton provided via `ShowdownService.get()`.
*
* @since February 27th, 2023
* @author landonjw
*/
interface ShowdownService {
/**
* The unique identifier for this showdown service.
*/
val id: ResourceLocation
/**
* The current status of the showdown service.
*/
val status: ShowdownStatus
/**
* The log of messages from the showdown service.
* This is accessed for use in [ShowdownDebugGUI]. It is not intended for general use
* and will be empty in production environments.
*/
val log: ShowdownDebugLog
/**
* Opens a connection to the showdown service.
*/
fun openConnection()
/**
* Closes the connection to the showdown service.
*/
fun closeConnection()
/**
* Restarts the connection to the showdown service.
*/
fun restartConnection()
/**
* Starts a battle in the showdown service.
*
* @param battle The battle to start.
* @param messages The initial messages for the battle.
*/
fun startBattle(battle: PokemonBattle, messages: Array<String>)
/**
* Sends messages to a specific battle in the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
fun send(battleId: UUID, messages: Array<String>)
/**
* Sends a message to the showdown service.
*
* @param message The message to send.
*/
fun send(message: String)
/**
* Retrieves the ability IDs from the showdown service.
*
* @return A `JsonArray` of ability IDs.
*/
fun getAbilityIds(): JsonArray
/**
* Retrieves the moves from the showdown service.
*
* @return A `JsonArray` of moves.
*/
fun getMoves(): JsonArray
/**
* Retrieves the item IDs from the showdown service.
*
* @return A `JsonArray` of item IDs.
*/
fun getItemIds(): JsonArray
/**
* Registers species in the showdown service.
*/
fun registerSpecies()
/**
* Registers bag items in the showdown service.
*/
fun registerBagItems()
/**
* Indicates that species have been initialized in the showdown service.
*/
fun indicateSpeciesInitialized() {}
companion object {
val service: ShowdownService by lazy { GraalShowdownService() }
private val services = mutableMapOf<ResourceLocation, ShowdownService>(
cobblemonResource("embedded") to GraalShowdownService(cobblemonResource("embedded")),
cobblemonResource("union") to UnionShowdownService(cobblemonResource("union"))
)
/**
* Retrieves the showdown service for the specified path.
*
* @param path The resource location of the showdown service.
* @return The showdown service.
* @throws IllegalStateException if no showdown service is found for the specified path.
*/
fun get(path: ResourceLocation = cobblemonResource("embedded")): ShowdownService {
return services[path] ?: error("No showdown service found for path $path")
}
/**
* Checks if a showdown service exists for the specified path.
*
* @param path The resource location of the showdown service.
* @return `true` if the showdown service exists, `false` otherwise.
*/
fun contains(path: ResourceLocation): Boolean {
return services.containsKey(path)
}
/**
* Retrieves all available showdown services.
*
* @return A list of all showdown services.
*/
fun all(): List<ShowdownService> {
return services.values.toList()
}
}
}
}
/**
* Enum representing the status of the showdown service.
*/
enum class ShowdownStatus {
CONNECTED, DISCONNECTED, RESTARTING
}
@@ -13,9 +13,12 @@ import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.runner.ShowdownDebugLog
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.google.gson.Gson
import com.google.gson.JsonArray
import net.minecraft.resources.ResourceLocation
import java.io.File
import java.io.IOException
import java.net.URI
@@ -38,12 +41,20 @@ import org.graalvm.polyglot.io.FileSystem
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service via
* GraalVM. This directly invokes JavaScript functions provided within the showdown service.
*
* @property id The unique identifier for this showdown service.
* @property context The GraalVM context for executing JavaScript.
* @property sendBattleMessageFunction The JavaScript function for sending battle messages.
* @property unbundler The unbundler for extracting necessary showdown files.
* @property gson The Gson instance for JSON operations.
* @property log The debug log for the showdown service.
* @property status The current status of the showdown service.
*
* @see {@code cobbled-exports/cobbled-index.js} within cobbledmon-showdown repository
* @see <a href="https://www.graalvm.org/">
* @since February 27, 2023
* @author Hiroku, landonjw
*/
class GraalShowdownService : ShowdownService {
class GraalShowdownService(override val id: ResourceLocation) : ShowdownService {
@Transient
lateinit var context: Context
@@ -54,12 +65,35 @@ class GraalShowdownService : ShowdownService {
@Transient
val gson = Gson()
override fun openConnection() {
override val log = ShowdownDebugLog()
override var status: ShowdownStatus = ShowdownStatus.DISCONNECTED
private set
/**
* Opens a connection to the showdown service.
*/
override fun openConnection() = openConnection(false)
/**
* Opens a connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the connection process.
*/
private fun openConnection(silent: Boolean) {
log.clear()
log.add(source = "Minecraft", message = "Opening showdown connection")
unbundler.attemptUnbundle()
createContext()
boot()
if (!silent) {
status = ShowdownStatus.CONNECTED
}
}
/**
* Creates the GraalVM context for executing JavaScript.
*/
private fun createContext() {
val wd = Paths.get("./showdown")
val access = HostAccess.newBuilder(HostAccess.EXPLICIT)
@@ -116,42 +150,126 @@ class GraalShowdownService : ShowdownService {
""".trimIndent())
}
override fun closeConnection() {
/**
* Closes the connection to the showdown service.
*/
override fun closeConnection() = closeConnection(false)
/**
* Closes the connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the connection process.
*/
private fun closeConnection(silent: Boolean) {
log.add(source = "Minecraft", message = "Closing showdown connection")
context.close()
if (!silent) {
this.status = ShowdownStatus.DISCONNECTED
}
}
/**
* Restarts the connection to the showdown service.
*/
override fun restartConnection() {
this.status = ShowdownStatus.RESTARTING
try {
closeConnection(true)
openConnection(true)
this.status = ShowdownStatus.CONNECTED
} catch (e: Exception) {
Cobblemon.LOGGER.error("Failed to restart showdown connection", e)
this.status = ShowdownStatus.DISCONNECTED
}
}
/**
* Boots the showdown service by evaluating the JavaScript entry point.
*/
private fun boot() {
log.add(source = "Minecraft", message = "Booting showdown")
context.eval("js", File("showdown/index.js").readText())
sendBattleMessageFunction = context.getBindings("js").getMember("sendBattleMessage")
}
/**
* Starts a battle in the showdown service.
*
* @param battle The battle to start.
* @param messages The initial messages for the battle.
*/
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
val startBattleFunction = context.getBindings("js").getMember("startBattle")
log.add(source = "Minecraft", message = "Starting battle ${battle.battleId}")
messages.forEach { message ->
log.add(source = "Minecraft", message = "(${battle.battleId}) $message")
}
startBattleFunction.execute(this, battle.battleId.toString(), messages)
}
/**
* Sends messages to a specific battle in the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
override fun send(battleId: UUID, messages: Array<String>) {
sendToShowdown(battleId, messages)
}
/**
* Sends a message to the showdown service.
*
* @param message The message to send.
*/
override fun send(message: String) {
log.add("Minecraft", message)
val regex = Regex("^\\(([^()]*)\\)")
val match = regex.find(message)
val battleId = match?.groups?.get(1)?.value ?: return
val endIndex = match.groups[1]!!.range.last + 1
sendToShowdown(UUID.fromString(battleId), arrayOf(message.substring(endIndex)))
}
/**
* Retrieves the ability IDs from the showdown service.
*
* @return A `JsonArray` of ability IDs.
*/
override fun getAbilityIds(): JsonArray {
val getCobbledAbilityIdsFn = context.getBindings("js").getMember("getCobbledAbilityIds")
val arrayResult = getCobbledAbilityIdsFn.execute().asString()
log.add(source = "Minecraft", message = "Getting ability ids")
return gson.fromJson(arrayResult, JsonArray::class.java)
}
/**
* Retrieves the moves from the showdown service.
*
* @return A `JsonArray` of moves.
*/
override fun getMoves(): JsonArray {
val getCobbledMovesFn = context.getBindings("js").getMember("getCobbledMoves")
log.add(source = "Minecraft", message = "Getting moves")
val arrayResult = getCobbledMovesFn.execute().asString()
return gson.fromJson(arrayResult, JsonArray::class.java)
}
/**
* Retrieves the item IDs from the showdown service.
*
* @return A `JsonArray` of item IDs.
*/
override fun getItemIds(): JsonArray {
val getCobbledItemIdsFn = context.getBindings("js").getMember("getCobbledItemIds")
log.add(source = "Minecraft", message = "Getting item ids")
val arrayResult = getCobbledItemIdsFn.execute().asString()
return gson.fromJson(arrayResult, JsonArray::class.java)
}
/**
* Registers species in the showdown service.
*/
override fun registerSpecies() {
val receiveSpeciesDataFn = this.context.getBindings("js").getMember("receiveSpeciesData")
val jsArray = this.context.eval("js", "new Array();")
@@ -164,32 +282,62 @@ class GraalShowdownService : ShowdownService {
}
}
}
log.add(source = "Minecraft", message = "Registering species")
receiveSpeciesDataFn.execute(jsArray)
}
/**
* Indicates that species have been initialized to the showdown service.
*/
override fun indicateSpeciesInitialized() {
val afterCobbledSpeciesInitFn = this.context.getBindings("js").getMember("afterCobbledSpeciesInit")
log.add(source = "Minecraft", message = "Indicating species initialized")
afterCobbledSpeciesInitFn.execute()
}
/**
* Registers bag items in the showdown service.
*/
override fun registerBagItems() {
val receiveBagItemDataFn = this.context.getBindings("js").getMember("receiveBagItemData")
for ((itemId, js) in BagItems.bagItemsScripts) {
log.add(source = "Minecraft", message = "Registering bag item $itemId")
receiveBagItemDataFn.execute(itemId, js.replace("\n", " "))
}
}
/**
* Sends messages to the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
private fun sendToShowdown(battleId: UUID, messages: Array<String>) {
messages.forEach { message ->
log.add(source = "Minecraft", message = "($battleId) $message")
}
sendBattleMessageFunction.execute(battleId.toString(), messages)
}
/**
* Receives messages from the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param message The message received.
*/
@Export
fun sendFromShowdown(battleId: String, message: String) {
log.add(source = "Showdown", message = "($battleId) $message")
ShowdownInterpreter.interpretMessage(UUID.fromString(battleId), message)
}
/**
* Logs messages from the showdown service.
*
* @param message The message to log.
*/
@Export
fun log(message: String) {
Cobblemon.LOGGER.info(message)
}
}
}
@@ -0,0 +1,381 @@
package com.cobblemon.mod.common.battles.runner.remote
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.UnionBattleActor
import com.cobblemon.mod.common.api.npc.NPCClasses
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.storage.party.PartyStore
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownDebugLog
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.HttpService
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.server
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.schedule.Activity
import java.net.URI
import java.net.http.HttpClient
import java.net.http.WebSocket
import java.util.*
import java.util.concurrent.CompletionStage
/**
* Implementation of the ShowdownService interface for use with the Union Block.
*
* This wraps around a WebSocket connection to a remote showdown service.
*
* @property id The unique identifier for this showdown service.
* @property host The host address of the showdown service.
* @property httpPort The port for HTTP communication with the showdown service.
* @property socketPort The port for WebSocket communication with the showdown service.
*
* @author landonjw
*/
class UnionShowdownService(
override val id: ResourceLocation,
val host: String = "localhost",
val httpPort: Int = 8003,
val socketPort: Int = 8002
) : ShowdownService {
override var status: ShowdownStatus = ShowdownStatus.DISCONNECTED
private set
override val log = ShowdownDebugLog()
private lateinit var webSocket: WebSocket
/**
* Opens a connection to the showdown service.
*/
override fun openConnection() = openConnection(false)
/**
* Opens a connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the connection process.
*/
private fun openConnection(silent: Boolean) {
log.clear()
log.add(source = "Minecraft", message = "Opening showdown connection")
try {
webSocket = HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(URI.create("ws://$host:$socketPort"), WebSocketListener(this))
.join()
// TODO: See if this is needed
registerSpecies()
indicateSpeciesInitialized()
registerBagItems()
if (!silent) {
this.status = ShowdownStatus.CONNECTED
}
} catch (e: Exception) {
Cobblemon.LOGGER.warn("Failed to open showdown connection to $host:$socketPort", e)
log.add(source = "Minecraft", message = "Failed to open showdown connection with error: ${e.message}")
if (!silent) {
this.status = ShowdownStatus.DISCONNECTED
}
}
}
/**
* Closes the connection to the showdown service.
*/
override fun closeConnection() = closeConnection(false)
/**
* Closes the connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the disconnection process.
*/
private fun closeConnection(silent: Boolean) {
log.add(source = "Minecraft", message = "Closing showdown connection")
try {
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "Goodbye!").join()
} catch (e: Exception) {
Cobblemon.LOGGER.warn("Failed to close showdown connection to $host:$socketPort", e)
log.add(source = "Minecraft", message = "Failed to close showdown connection to $host:$socketPort with error: ${e.message}")
}
finally {
if (!silent) {
this.status = ShowdownStatus.DISCONNECTED
}
}
}
/**
* Restarts the connection to the showdown service.
*/
override fun restartConnection() {
this.status = ShowdownStatus.RESTARTING
closeConnection(true)
openConnection(true)
}
/**
* Starts a battle in the showdown service.
*
* @param battle The battle to start.
* @param messages The initial messages for the battle.
*/
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
send("[${battle.battleId}]>start-battle")
send(battle.battleId, messages)
}
/**
* Sends messages to a specific battle in the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
override fun send(battleId: UUID, messages: Array<String>) {
messages.forEach { send("[$battleId]$it") }
}
/**
* Sends a message to the showdown service.
*
* @param message The message to send.
*/
override fun send(message: String) {
webSocket.sendText(message, true)
}
/**
* Retrieves the ability IDs from the showdown service.
*
* @return A `JsonArray` of ability IDs.
*/
override fun getAbilityIds(): JsonArray {
TODO("Not yet implemented")
}
/**
* Retrieves the moves from the showdown service.
*
* @return A `JsonArray` of moves.
*/
override fun getMoves(): JsonArray {
TODO("Not yet implemented")
}
/**
* Retrieves the item IDs from the showdown service.
*
* @return A `JsonArray` of item IDs.
*/
override fun getItemIds(): JsonArray {
TODO("Not yet implemented")
}
/**
* Registers species in the showdown service.
*/
override fun registerSpecies() {
HttpService.post<Unit>(
url = "http://$host:$httpPort/Species",
body = PokemonSpecies.species.map { PokemonSpecies.ShowdownSpecies(it, null) }
)
}
/**
* Registers bag items in the showdown service.
*/
override fun registerBagItems() {
HttpService.post<Unit>(
url = "http://$host:$httpPort/BagItems",
body = BagItems.bagItemsScripts.map { BagItem(it.key, it.value) }.toList()
)
}
override fun indicateSpeciesInitialized() {
HttpService.post<Unit>(url = "http://$host:$httpPort/InitializeDex")
}
/**
* WebSocket listener for handling events from the showdown service.
*
* @property service The UnionShowdownService instance.
*/
class WebSocketListener(val service: UnionShowdownService) : WebSocket.Listener {
val builder = StringBuilder()
override fun onOpen(webSocket: WebSocket) {
service.log.add(source = "Showdown", message = "Connected to server at ${service.host}:${service.socketPort}")
super.onOpen(webSocket)
}
/**
* Starts a battle based on the received instruction.
*
* @param instruction The instruction to start the battle.
*/
fun startBattle(instruction: String) {
val arguments = instruction.replace("|start-battle|", "").split("|")
if (arguments.size != 7) return
val battleId = UUID.fromString(arguments[0])
val playerIdA = UUID.fromString(arguments[1])
val playerNameA = arguments[2]
val playerIdB = UUID.fromString(arguments[3])
val playerNameB = arguments[4]
val teamA = JsonParser.parseString(arguments[5]).asJsonObject
val teamB = JsonParser.parseString(arguments[6]).asJsonObject
val playerA = server()?.playerList?.getPlayer(playerIdA)
val playerB = server()?.playerList?.getPlayer(playerIdB)
println(arguments[5])
println(arguments[6])
if (playerA == null && playerB == null) {
Cobblemon.LOGGER.warn("Tried to start battle with no players present on server")
return
}
val registryAccess = server()?.registryAccess() ?: return
val playerAParty = PartyStore(UUID.randomUUID()).loadFromJSON(teamA, registryAccess)
val playerBParty = PartyStore(UUID.randomUUID()).loadFromJSON(teamB, registryAccess)
val playerATeam = toShowdownBattleTeam(playerAParty.toList()).onEach {
it.effectedPokemon.level = 100
it.effectedPokemon.heal()
}
val playerBTeam = toShowdownBattleTeam(playerBParty.toList()).onEach {
it.effectedPokemon.level = 100
it.effectedPokemon.heal()
}
if (playerA != null && playerB != null) {
val playerActorA = PlayerBattleActor(playerIdA, playerATeam)
val playerActorB = PlayerBattleActor(playerIdB, playerBTeam)
BattleRegistry.startBattle(
battleId = battleId,
battleFormat = BattleFormat.GEN_9_SINGLES,
side1 = BattleSide(playerActorA),
side2 = BattleSide(playerActorB),
sendMessages = false,
service = service
)
}
else if (playerA != null && playerB == null) {
val playerActorA = PlayerBattleActor(playerIdA, playerATeam)
val npc = NPCEntity(playerA.level())
npc.moveTo(playerA.x + 5, playerA.y, playerA.z, playerA.yRot, (playerA.xRot + 180f) % 360f)
npc.npc = NPCClasses.getByIdentifier(cobblemonResource("standard"))!!
npc.loadTextureFromGameProfileName(playerNameB)
npc.initialize(1)
npc.config.setDirectly("yaw", DoubleValue((playerA.xRot + 90f) % 360f))
npc.uuid = playerIdB
npc.customName = playerNameB.text()
npc.isInvulnerable = true
npc.isMovable = false
playerA.level().addFreshEntity(npc)
val playerActorB = UnionBattleActor(npc, playerBTeam)
BattleRegistry.startBattle(
battleId = battleId,
battleFormat = BattleFormat.GEN_9_SINGLES,
side1 = BattleSide(playerActorA),
side2 = BattleSide(playerActorB),
sendMessages = false,
service = service
)
}
else {
val npc = NPCEntity(playerB!!.level())
npc.moveTo(playerB.x + 5, playerB.y, playerB.z, playerB.yRot, (playerB.xRot + 180f) % 360f)
npc.npc = NPCClasses.getByIdentifier(cobblemonResource("standard"))!!
npc.loadTextureFromGameProfileName(playerNameA)
npc.initialize(1)
npc.uuid = playerIdA
npc.customName = playerNameA.text()
npc.isInvulnerable = true
npc.isMovable = false
npc.config.setDirectly("yaw", DoubleValue((playerB.xRot + 90f) % 360f))
playerB.level().addFreshEntity(npc)
val playerActorA = UnionBattleActor(npc, playerATeam)
val playerActorB = PlayerBattleActor(playerIdB, playerBTeam)
BattleRegistry.startBattle(
battleId = battleId,
battleFormat = BattleFormat.GEN_9_SINGLES,
side1 = BattleSide(playerActorA),
side2 = BattleSide(playerActorB),
sendMessages = false,
service = service
)
}
}
/**
* Converts a list of Pokemon to a list of BattlePokemon for the showdown service.
*
* @param team The list of Pokemon to convert.
* @return A list of BattlePokemon.
*/
fun toShowdownBattleTeam(team: List<Pokemon>): List<BattlePokemon> {
return team.map { pokemon ->
BattlePokemon(
originalPokemon = pokemon,
effectedPokemon = pokemon.clone(newUUID = false),
postBattleEntityOperation = { entity -> entity.discard() }
)
}
}
override fun onText(webSocket: WebSocket, data: CharSequence, last: Boolean): CompletionStage<*>? {
if (last) {
builder.append(data)
val message = builder.toString()
service.log.add(source = "Showdown", message = message)
if (data.startsWith("|start-battle|")) {
startBattle(data.toString())
}
else {
val battleIdRegex = Regex("^\\[([^]]+)]")
val match = battleIdRegex.find(message) ?: return null
val battleId = match.groupValues[1]
val instruction = message.replace("[$battleId]", "")
ShowdownInterpreter.interpretMessage(UUID.fromString(battleId), instruction)
}
builder.clear()
}
else {
builder.append(data)
}
return super.onText(webSocket, data, last)
}
override fun onError(webSocket: WebSocket, error: Throwable) {
service.log.add(source = "Showdown", message = "Error: ${error.message}")
Cobblemon.LOGGER.warn("Error in showdown service ${service.id} websocket", error)
super.onError(webSocket, error)
}
override fun onClose(webSocket: WebSocket, statusCode: Int, reason: String): CompletionStage<*>? {
service.log.add(source = "Showdown", message = "Closed connection with status $statusCode and reason $reason")
return super.onClose(webSocket, statusCode, reason)
}
}
/**
* Data class representing a bag item. We use this for serialization to the showdown service.
*
* @property id The unique identifier of the bag item.
* @property script The script associated with the bag item.
*/
private data class BagItem(val id: String, val script: String)
}
@@ -1,169 +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.battles.runner.socket
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.google.gson.Gson
import com.google.gson.JsonArray
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.InetAddress
import java.net.Socket
import java.nio.charset.Charset
import java.util.UUID
/**
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service via
* a socket client.
*
* This is primarily used for debugging purposes, but could be extended in the future to provide
* a means of connecting to a remote Showdown server. This does not provide any fault handling in the
* event that the server goes down.
*
* When messages are sent to showdown, this will await a response from showdown.
* The protocol for messages sent from showdown is <length of characters in payload><payload>,
* and a payload length of 0 indicates that there is no response.
*
* @see {@code sim/cobbled-debug-server.ts} within cobblemon-showdown repository
* @since February 27, 2023
* @author landonjw
*/
class SocketShowdownService(val host: String = "localhost", val port: Int = 18468, val localPort: Int = 0) : ShowdownService {
private lateinit var socket: Socket
private lateinit var writer: OutputStreamWriter
private lateinit var reader: BufferedReader
val gson = Gson()
override fun openConnection() {
socket = Socket(InetAddress.getLocalHost(), port, InetAddress.getLocalHost(), localPort)
writer = socket.getOutputStream().writer(charset = Charset.forName("ascii"))
reader = BufferedReader(InputStreamReader(socket.getInputStream()))
}
override fun closeConnection() {
socket.close()
}
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
writer.write(">startbattle ${battle.battleId}\n")
acknowledge { Cobblemon.LOGGER.error("Failed to start battle!") }
send(battle.battleId, messages)
}
override fun send(battleId: UUID, messages: Array<String>) {
for (message in messages) {
writer.write("$battleId~$message\n")
writer.flush()
readBattleInput().forEach { interpretMessage(battleId, it) }
}
}
private fun read(reader: BufferedReader, size: Int): String {
val buffer = CharArray(size)
while (true) {
if(reader.read(buffer) == 0) continue
return String(buffer)
}
}
private fun readMessage(): String {
val payloadSize = read(reader, 8).toInt()
val payload = read(reader, payloadSize)
return payload
}
private fun readBattleInput(): List<String> {
val lines = mutableListOf<String>()
val numLines = read(reader, 8).toInt()
if (numLines != 0) {
for (i in 0 until numLines) {
lines.add(readMessage())
}
}
return lines
}
private fun interpretMessage(battleId: UUID, message: String) {
ShowdownInterpreter.interpretMessage(battleId, message)
}
override fun getAbilityIds(): JsonArray {
writer.write(">getCobbledAbilityIds")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
}
override fun getMoves(): JsonArray {
writer.write(">getCobbledMoves\n")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
}
override fun getItemIds(): JsonArray {
writer.write(">getCobbledItemIds")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
}
private fun sendSpeciesData(species: Species, form: FormData?) {
writer.write(">receiveSpeciesData ${gson.toJson(PokemonSpecies.ShowdownSpecies(species, form))}\n")
acknowledge()
}
private fun sendBagItem(itemId: String, js: String) {
writer.write(">receiveBagItemData $itemId $js")
acknowledge { Cobblemon.LOGGER.error("Failed to send bag item to Showdown: $itemId") }
}
override fun registerSpecies() {
writer.write(">resetSpeciesData\n")
acknowledge()
PokemonSpecies.species.forEach { species ->
sendSpeciesData(species, null)
species.forms.forEach { form ->
if (form != species.standardForm) {
sendSpeciesData(species, form)
}
}
}
}
fun acknowledge(ifFails: () -> Unit = {}) {
writer.flush()
val ack = CharArray(3)
reader.read(ack)
if (String(ack) != "ACK") {
ifFails()
}
}
override fun registerBagItems() {
for ((itemId, js) in BagItems.bagItemsScripts) {
sendBagItem(itemId, js.replace("\n", " "))
}
}
override fun indicateSpeciesInitialized() {
writer.write(">afterCobbledSpeciesInit")
acknowledge()
}
}
@@ -0,0 +1,330 @@
package com.cobblemon.mod.common.client.gui.debug
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.debug.ShowdownDebugGUI.Companion.activeShowdownService
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.RequestShowdownInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownActionPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownInputPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.platform.ClipboardManager
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.components.ObjectSelectionList
import net.minecraft.client.gui.components.ObjectSelectionList.Entry
import net.minecraft.client.gui.screens.Screen
import net.minecraft.locale.Language
import net.minecraft.network.chat.Component
import net.minecraft.util.FormattedCharSequence
import java.awt.Color
import java.util.*
/**
* GUI screen for debugging the Showdown service in the Cobblemon mod.
* This screen allows users to view and interact with the Showdown service logs and status.
*
* @property status The current status of the showdown service.
* @property service The resource location of the showdown service.
* @property messages A list of messages from the active showdown service currently being refreshed.
* @property availableServices A list of available showdown services. This allows the client to switch which server they are viewing.
* @property requestProcessedAt The date and time when the request was processed.
* @property includesStartOfLog Indicates if the messages include the start of the log.
*
* @author landonjw
*/
class ShowdownDebugGUI : Screen(lang("ui.debug.showdown")), CobblemonRenderable {
companion object {
var activeShowdownService = cobblemonResource("embedded")
private set
}
private var showdownStatus: ShowdownStatus? = null
val log = mutableListOf<String>()
var availableServices = listOf(cobblemonResource("embedded"))
var lastProcessedLogTime: Date? = null
val outputWindow = addRenderableWidget(ShowdownOutputList())
val serviceButton = addRenderableWidget(
Button.builder(Component.literal("Service: $activeShowdownService")) {
cycleActiveService()
}.bounds(10, 10, 200, 20).build()
)
val startButton = addRenderableWidget(
Button.builder(Component.literal("Start")) { button ->
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.START
))
}.bounds(10, 40, 50, 20).build()
)
val stopButton = addRenderableWidget(
Button.builder(Component.literal("Stop")) { button ->
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.STOP
))
}.bounds(70, 40, 50, 20).build()
)
val restartButton = addRenderableWidget(
Button.builder(Component.literal("Restart")) { button ->
this.showdownStatus = ShowdownStatus.RESTARTING
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.RESTART
))
}.bounds(130, 40, 50, 20).build()
)
val startNPCBattleButton = addRenderableWidget(
Button.builder(Component.literal("Queue")) { button ->
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.QUEUE
))
}.bounds(10, 70, 100, 20).build()
)
val clipboardManager = ClipboardManager()
var timer = Timer()
init {
addRenderableWidget(
Button.builder(Component.literal("Copy to Clipboard")) Clipboard@{
if (log.isEmpty()) return@Clipboard
clipboardManager.setClipboard(Minecraft.getInstance().window.window, log.joinToString("\n"))
}.bounds(170, Minecraft.getInstance().window.guiScaledHeight - 40, 100, 30).build()
)
addRenderableWidget(ShowdownInputWidget(this))
startTimer()
}
/**
* Cycles through the available showdown services.
*/
fun cycleActiveService() {
if (availableServices.size == 1) return
val currentIndex = availableServices.indexOf(activeShowdownService)
activeShowdownService = availableServices[(currentIndex + 1) % availableServices.size]
serviceButton.message = Component.literal("Service: $activeShowdownService")
outputWindow.reset()
log.clear()
lastProcessedLogTime = null
startTimer()
}
/**
* Starts the timer for periodically refreshing the showdown info.
*/
fun startTimer() {
timer.cancel()
timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
CobblemonNetwork.sendToServer(RequestShowdownInfoPacket(activeShowdownService, lastProcessedLogTime))
}
}, 0, 500)
}
override fun onClose() {
timer.cancel()
super.onClose()
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, width, height, Color(0, 0, 0, 100).rgb)
super.render(guiGraphics, mouseX, mouseY, partialTick)
if (showdownStatus != null) {
guiGraphics.fill(
190,
45,
200,
55,
when (showdownStatus!!) {
ShowdownStatus.DISCONNECTED -> Color(255, 0, 0, 255).rgb
ShowdownStatus.RESTARTING -> Color(255, 255, 0, 255).rgb
ShowdownStatus.CONNECTED -> Color(0, 255, 0, 255).rgb
}
)
drawScaledText(
context = guiGraphics,
text = Component.literal(showdownStatus!!.name.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
}),
x = 205,
y = 47
)
}
}
/**
* Refreshes the showdown info with the data from the given packet.
*
* @param packet The packet containing the refreshed showdown info.
*/
fun refreshShowdownInfo(packet: ShowdownRefreshInfoPacket) {
// Prevent false positives from old packets
if (activeShowdownService != packet.service) return
if (packet.includesStartOfLog) {
log.clear()
outputWindow.reset()
}
packet.messages.forEach {
log.add(it)
outputWindow.addNewEntry(it.text())
}
lastProcessedLogTime = packet.requestProcessedAt
showdownStatus = packet.status
when (showdownStatus) {
ShowdownStatus.CONNECTED -> {
startButton.active = false
stopButton.active = true
restartButton.active = true
startNPCBattleButton.active = true
}
ShowdownStatus.DISCONNECTED -> {
startButton.active = true
stopButton.active = false
restartButton.active = false
startNPCBattleButton.active = false
}
ShowdownStatus.RESTARTING -> {
startButton.active = false
stopButton.active = false
restartButton.active = false
startNPCBattleButton.active = false
}
else -> {
startButton.active = false
stopButton.active = false
restartButton.active = false
startNPCBattleButton.active = false
}
}
this.availableServices = packet.availableServices
}
}
/**
* List widget for displaying the output log of the Showdown service.
*/
class ShowdownOutputList : ObjectSelectionList<OutputEntry>(
Minecraft.getInstance(),
Minecraft.getInstance().window.guiScaledWidth - 280 - 10,
Minecraft.getInstance().window.guiScaledHeight - 10 - 40,
10,
14
) {
/**
* Adds a new entry to the output list.
*
* @param text The text to add as a new entry.
*/
fun addNewEntry(text: Component) {
val line = text.copy()
val textRenderer = Minecraft.getInstance().font
val lines = Language.getInstance().getVisualOrder(textRenderer.splitter.splitLines(line, rowWidth, line.style))
val fullyScrolledDown = maxScroll - scrollAmount < 10
lines.forEach { addEntry(OutputEntry(it)) }
if (fullyScrolledDown) {
scrollAmount = maxScroll.toDouble()
}
}
override fun getSelected(): OutputEntry? = null
override fun getRowWidth(): Int {
return Minecraft.getInstance().window.guiScaledWidth - 280 - 10 - 10
}
override fun getScrollbarPosition(): Int {
return this.x + rowWidth + 5
}
override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
setRectangle(
Minecraft.getInstance().window.guiScaledWidth - 280 - 10,
Minecraft.getInstance().window.guiScaledHeight - 10 - 10 - 40,
280,
10
)
this.x = 280
super.renderWidget(guiGraphics, mouseX, mouseY, partialTick)
}
/**
* Resets the output list by clearing all entries.
*/
fun reset() {
super.clearEntries()
}
}
/**
* Entry for the output list, representing a single line of text.
*
* @param line The formatted line of text to display.
*/
class OutputEntry(val line: FormattedCharSequence) : Entry<OutputEntry>() {
override fun getNarration() = "".text()
override fun render(
context: GuiGraphics,
index: Int,
rowTop: Int,
rowLeft: Int,
rowWidth: Int,
rowHeight: Int,
mouseX: Int,
mouseY: Int,
isHovered: Boolean,
partialTicks: Float
) {
drawScaledText(
context,
line,
rowLeft,
rowTop - 2
)
}
override fun isFocused() = false
}
/**
* Input widget for entering commands to the Showdown service.
*/
class ShowdownInputWidget(val gui: ShowdownDebugGUI) : EditBox(
Minecraft.getInstance().font,
280,
Minecraft.getInstance().window.guiScaledHeight - 40,
Minecraft.getInstance().window.guiScaledWidth - 280 - 10,
30,
Component.literal("Enter command")
) {
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
val result = super.keyPressed(keyCode, scanCode, modifiers)
if (keyCode == 257 && this.value.isNotEmpty()) {
CobblemonNetwork.sendToServer(ShowdownInputPacket(
service = activeShowdownService,
input = this.value
))
deleteCharsToPos(0)
CobblemonNetwork.sendToServer(RequestShowdownInfoPacket(activeShowdownService, gui.lastProcessedLogTime))
}
return result
}
}
@@ -18,6 +18,7 @@ import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.world.item.ItemStack
import net.minecraft.world.phys.Vec3
import org.joml.Quaternionf
import org.joml.Vector3f
@@ -115,9 +116,10 @@ class ModelWidget(
heldItemRenderer.renderOnModel(
state.currentModel!!,
heldItem?: ItemStack.EMPTY,
state.locatorStates,
state,
matrices,
context.bufferSource()
context.bufferSource(),
Vec3(0.0,-90.0,-90.0)
)
matrices.popPose()
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.client.keybind.keybinds
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.debug.ShowdownDebugGUI
import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinding
import com.cobblemon.mod.common.client.keybind.KeybindCategories
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
@@ -26,8 +27,10 @@ object DebugKeybindings {
TranslateRightKeybinding(),
TranslateUpKeybinding(),
TranslateDownKeybinding(),
PrintModelSettingsKeybinding()
PrintModelSettingsKeybinding(),
ToggleShowdownDebugGUIKeybinding()
)
class ScaleUpKeybinding: CobblemonKeyBinding(
"key.cobblemon.scaleportraitup",
InputConstants.Type.KEYSYM,
@@ -145,4 +148,15 @@ object DebugKeybindings {
}
}
}
}
class ToggleShowdownDebugGUIKeybinding : CobblemonKeyBinding(
"key.cobblemon.toggleshowdowndebuggui",
InputConstants.Type.KEYSYM,
InputConstants.KEY_APOSTROPHE,
KeybindCategories.COBBLEMON_DEBUG_CATEGORY
) {
override fun onPress() {
Minecraft.getInstance().setScreen(ShowdownDebugGUI())
}
}
}
@@ -0,0 +1,26 @@
package com.cobblemon.mod.common.client.net.debug.showdown
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.debug.ShowdownDebugGUI
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
import net.minecraft.client.Minecraft
/**
* Handles the reception of a [ShowdownRefreshInfoPacket] on the client side.
* This handler updates the Showdown Debug GUI with the latest showdown information.
* @author landonjw
*/
object ShowdownRefreshInfoHandler : ClientNetworkPacketHandler<ShowdownRefreshInfoPacket> {
/**
* Handles the received [ShowdownRefreshInfoPacket] by refreshing the showdown information
* in the [ShowdownDebugGUI] if it is currently open.
*
* @param packet The received packet containing the showdown information.
* @param client The Minecraft client instance.
*/
override fun handle(packet: ShowdownRefreshInfoPacket, client: Minecraft) {
if (client.screen is ShowdownDebugGUI) {
(client.screen as ShowdownDebugGUI).refreshShowdownInfo(packet)
}
}
}
@@ -11,24 +11,24 @@ package com.cobblemon.mod.common.client.render.item
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.tags.CobblemonItemTags.WEARABLE_FACE_ITEMS
import com.cobblemon.mod.common.api.tags.CobblemonItemTags.WEARABLE_HAT_ITEMS
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.client.render.MatrixWrapper
import com.cobblemon.mod.common.client.render.models.blockbench.NullObjectParser
import com.cobblemon.mod.common.client.render.models.blockbench.PosableModel
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.math.Axis
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.entity.ItemRenderer
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.item.ItemDisplayContext
import net.minecraft.world.item.ItemStack
import net.minecraft.world.phys.Vec3
class HeldItemRenderer() {
private val itemRenderer = Minecraft.getInstance().itemRenderer
private var displayContext = ItemDisplayContext.FIXED
private var scale = 1.0f
class HeldItemRenderer {
private val itemRenderer: ItemRenderer = Minecraft.getInstance().itemRenderer
private var displayContext: ItemDisplayContext = ItemDisplayContext.FIXED
companion object {
const val ITEM_FACE = "item_face"
@@ -37,99 +37,130 @@ class HeldItemRenderer() {
}
fun render(
entity: PokemonEntity?,
entity: LivingEntity?,
model: PosableModel,
item: ItemStack,
locators: Map<String, MatrixWrapper>,
poseStack: PoseStack,
buffer: MultiBufferSource,
light: Int,
seed: Int
seed: Int,
rotation: Vec3
) {
if (item.isEmpty) return
displayContext = ItemDisplayContext.FIXED
poseStack.pushPose()
when {
//item_face locator
(locators.containsKey(ITEM_FACE) && item.`is`(WEARABLE_FACE_ITEMS)) -> {
displayContext = model.getLocatorDisplayContext(ITEM_FACE) ?: ItemDisplayContext.HEAD
poseStack.mulPose(locators[ITEM_FACE]!!.matrix)
poseStack.translate(0f, 0f, .28f * scale)
poseStack.scale(0.7f * scale, 0.7f * scale, 0.7f * scale)
poseStack.translate(0f, 0f, .28f)
poseStack.scale(0.7f, 0.7f, 0.7f)
}
(locators.containsKey("item_hat") && item.`is`(WEARABLE_HAT_ITEMS)) -> {
//item_hat locator
(locators.containsKey(ITEM_HAT) && item.`is`(WEARABLE_HAT_ITEMS)) -> {
displayContext = model.getLocatorDisplayContext(ITEM_HAT) ?: ItemDisplayContext.HEAD
poseStack.mulPose(locators["item_hat"]!!.matrix)
poseStack.translate(0f, -0.26f * scale, 0f)
poseStack.scale(.68f * scale, .68f * scale, .68f * scale)
poseStack.mulPose(locators[ITEM_HAT]!!.matrix)
poseStack.translate(0f, -0.26f, 0f)
poseStack.scale(.68f, .68f, .68f)
}
(locators.containsKey("item")) -> {
updateModifiers("item",locators)
poseStack.mulPose(locators["item"]!!.matrix)
//item locator
(locators.containsKey(ITEM)) -> {
displayContext = model.getLocatorDisplayContext(ITEM) ?: ItemDisplayContext.FIXED
when (displayContext) {
ItemDisplayContext.FIXED -> {
poseStack.translate(0f, 0.01666f * scale, 0f)
poseStack.scale(.5f * scale, .5f * scale, .5f * scale)
poseStack.mulPose(Axis.XP.rotationDegrees(90f))
}
ItemDisplayContext.THIRD_PERSON_RIGHT_HAND -> {
poseStack.translate(0.075f* scale, 0f * scale, -0.05f* scale)
poseStack.scale(scale, scale, scale)
poseStack.mulPose(Axis.XP.rotationDegrees(-90f))
poseStack.mulPose(Axis.YP.rotationDegrees(-90f))
}
else -> {
poseStack.scale(scale, scale, scale)
}
}
poseStack.mulPose(locators[ITEM]!!.matrix)
applyContextTransformation(poseStack, rotation)
}
else -> { // Don't render any item
// Don't render any item
else -> {
poseStack.popPose()
return
}
}
itemRenderer.renderStatic(entity, item, displayContext, false, poseStack, buffer, null, light, OverlayTexture.NO_OVERLAY, seed)
itemRenderer.renderStatic(entity, item, displayContext, (displayContext==ItemDisplayContext.THIRD_PERSON_LEFT_HAND), poseStack, buffer, null, light, OverlayTexture.NO_OVERLAY, seed)
poseStack.popPose()
}
private fun renderAnimationItem(
model: PosableModel,
state: PosableState,
locators: Map<String, MatrixWrapper>,
poseStack: PoseStack,
buffer: MultiBufferSource,
light: Int,
entity: LivingEntity?,
rotation: Vec3
) {
state.itemRenderingLocations.forEach { (locatorName, item) ->
poseStack.pushPose()
if (locators.containsKey(locatorName)) {
displayContext = model.getLocatorDisplayContext(locatorName)?: ItemDisplayContext.FIXED
poseStack.mulPose(locators[locatorName]!!.matrix)
applyContextTransformation(poseStack, rotation)
}
itemRenderer.renderStatic(entity, item, displayContext, (displayContext==ItemDisplayContext.THIRD_PERSON_LEFT_HAND), poseStack, buffer, null, light, OverlayTexture.NO_OVERLAY, 0)
poseStack.popPose()
}
}
fun renderOnModel(
model: PosableModel,
item: ItemStack,
locators: Map<String, MatrixWrapper>,
state: PosableState,
poseStack: PoseStack,
buffer: MultiBufferSource,
light: Int = LightTexture.pack(11, 7)
){
if (!item.`is`(CobblemonItemTags.HIDDEN_ITEMS)) render(null, model, item, locators, poseStack, buffer, light, 0)
rotation: Vec3 = Vec3(0.0,0.0,0.0),
light: Int = LightTexture.pack(11, 7),
) {
val locators: Map<String, MatrixWrapper> = state.locatorStates
if (state.itemRenderingLocations.isNotEmpty()) {
renderAnimationItem(model, state, locators, poseStack, buffer, light, null, rotation)
}
else if (!item.`is`(CobblemonItemTags.HIDDEN_ITEMS)) {
render(null, model, item, locators, poseStack, buffer, light, 0, rotation)
}
}
fun renderOnEntity(
entity: PokemonEntity,
delegate: PokemonClientDelegate,
entity: LivingEntity,
item: ItemStack,
model: PosableModel,
state: PosableState,
poseStack: PoseStack,
buffer: MultiBufferSource,
light: Int
light: Int,
rotation: Vec3 = Vec3(0.0,0.0,0.0)
) {
val shownItem = entity.shownItem
val locators: Map<String, MatrixWrapper> = delegate.locatorStates
render(entity, model, shownItem, locators, poseStack, buffer, light, 0)
val locators: Map<String, MatrixWrapper> = state.locatorStates
if (state.itemRenderingLocations.isNotEmpty()) {
renderAnimationItem(model, state, locators, poseStack, buffer, light, entity, rotation)
}
else {
render(entity, model, item, locators, poseStack, buffer, light, 0, rotation)
}
}
private fun updateModifiers(name: String, locators: Map<String, MatrixWrapper>) {
locators.forEach { (locator: String, m: MatrixWrapper) ->
val modifiers: Map<String, Float>
if (locator.startsWith("_null_$name[")) {
modifiers = NullObjectParser.parseNullObject(locator).modifiers
if (modifiers.containsKey("scale")) scale = modifiers["scale"]!!
if (modifiers.containsKey("mode")) displayContext = ItemDisplayContext.entries[((modifiers["mode"]!!).toInt()) % 9]
return
private fun applyContextTransformation(poseStack: PoseStack, rotation: Vec3) {
when (displayContext) {
ItemDisplayContext.FIXED -> {
poseStack.mulPose(Axis.XP.rotationDegrees(90.0f))
poseStack.scale(.5f, .5f, .5f )
poseStack.translate(0f, 0.01666f, 0f)
}
ItemDisplayContext.THIRD_PERSON_RIGHT_HAND, ItemDisplayContext.THIRD_PERSON_LEFT_HAND -> {
poseStack.mulPose(Axis.XP.rotationDegrees((rotation.x).toFloat()))
poseStack.mulPose(Axis.YP.rotationDegrees((rotation.y).toFloat()))
poseStack.mulPose(Axis.ZP.rotationDegrees((rotation.z).toFloat()))
poseStack.translate(0.025f * if (displayContext==ItemDisplayContext.THIRD_PERSON_LEFT_HAND) 1 else -1 , 0.0f, 0.0f)
}
ItemDisplayContext.HEAD -> {
poseStack.mulPose(Axis.XP.rotationDegrees(90.0f))
poseStack.scale(.6f, .6f, .6f)
poseStack.translate(0f, -0.6f, 0f)
}
else -> {}
}
}
}
@@ -37,6 +37,7 @@ import net.minecraft.nbt.Tag
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.phys.Vec3
class PokemonOnShoulderRenderer<T : Player>(renderLayerParent: RenderLayerParent<T, PlayerModel<T>>) : RenderLayer<T, PlayerModel<T>>(renderLayerParent) {
@@ -163,9 +164,10 @@ class PokemonOnShoulderRenderer<T : Player>(renderLayerParent: RenderLayerParent
heldItemRenderer.renderOnModel(
model,
shoulderData.shownItem,
state.locatorStates,
state,
matrixStack,
buffer
buffer,
Vec3(0.0,-90.0,-90.0)
)
model.setDefault()
@@ -1,50 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.models.blockbench
/**
* Uses the name of null objects in a model to get modifier values for a locator.
* @author Josh
*/
object NullObjectParser {
fun parseNullObject(input: String): NullObjectModifier {
// Extract modifiedLocator
val modifiedLocator = input.replaceFirst("^_null_".toRegex(), "").replace("\\[.*\\]$".toRegex(), "")
// Extract modifiers
val modifiers: MutableMap<String, Float> = HashMap()
val start = input.indexOf('[')
val end = input.indexOf(']')
if (start != -1 && end != -1 && end > start) {
val modifierPart = input.substring(start + 1, end)
val modifierPairs = modifierPart.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (pair in modifierPairs) {
val keyValue = pair.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (keyValue.size == 2) {
val key = keyValue[0].trim { it <= ' ' }
try {
val value = keyValue[1].trim { it <= ' ' }.toFloat()
modifiers[key] = value
} catch (e: NumberFormatException) {
System.err.println("Invalid number format for: " + keyValue[1])
}
}
}
}
return NullObjectModifier(modifiedLocator, modifiers)
}
/**
* Stores modifiers for a particular locator.
*/
class NullObjectModifier(val locatorName: String, val modifiers: Map<String, Float>)
}
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.client.ClientMoLangFunctions.animationFunctions
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.entity.NPCClientDelegate
import com.cobblemon.mod.common.client.entity.PokemonClientDelegate
import com.cobblemon.mod.common.client.render.AnimatedModelTextureSupplier
import com.cobblemon.mod.common.client.render.ModelLayer
@@ -56,6 +57,7 @@ import net.minecraft.client.renderer.texture.DynamicTexture
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.item.ItemDisplayContext
import net.minecraft.world.phys.Vec3
import org.joml.Matrix4f
@@ -36,14 +36,18 @@ import com.cobblemon.mod.common.client.render.models.blockbench.quirk.QuirkData
import com.cobblemon.mod.common.entity.PosableEntity
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.mojang.brigadier.StringReader
import net.minecraft.client.Minecraft
import net.minecraft.client.multiplayer.ClientLevel
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.commands.arguments.item.ItemParser
import net.minecraft.sounds.SoundEvent
import net.minecraft.sounds.SoundSource
import net.minecraft.world.entity.Entity
import net.minecraft.world.item.ItemStack
import net.minecraft.world.phys.Vec3
import org.joml.Matrix4f
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
/**
@@ -124,6 +128,10 @@ abstract class PosableState : Schedulable {
/** This gets called 500 million times so use a mutable value for runtime */
private val reusableAnimTime = DoubleValue(0.0)
val itemRenderingLocations = mutableMapOf<String, ItemStack>()
// var itemRenderingLocation = ""
// var animationItem : ItemStack? = null
/** All of the MoLang functions that can be applied to something with this state. */
val functions = QueryStruct(hashMapOf())
.addFunction("anim_time") {
@@ -148,6 +156,24 @@ abstract class PosableState : Schedulable {
)
}
}
.addFunction("render_item") { params ->
if (params.get<MoValue>(0) !is StringValue) return@addFunction Unit
val client = Minecraft.getInstance().connection ?: return@addFunction Unit
val result = ItemParser(client.registryAccess()).parse(StringReader(params.getString(0)))
val item = ItemStack(result.item)
item.applyComponents(result.components)
val renderLocation = if (params.contains(1)) params.getString(1) else "item"
if (!item.isEmpty && locatorStates.containsKey(renderLocation)) {
itemRenderingLocations.put(renderLocation, item)
} else {
if (itemRenderingLocations.containsKey(renderLocation)) itemRenderingLocations.remove(renderLocation)
return@addFunction Unit
}
}
.addFunction("clear_items") { itemRenderingLocations.clear() }
.addFunction("play_animation") { params ->
val animationParameter = params.get<MoValue>(0)
val animation = if (animationParameter is ObjectValue<*>) {
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.client.render.npc
import com.cobblemon.mod.common.client.entity.NPCClientDelegate
import com.cobblemon.mod.common.client.render.item.HeldItemRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.PosableEntityModel
import com.cobblemon.mod.common.client.render.models.blockbench.npc.PosableNPCModel
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
@@ -19,6 +20,7 @@ import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.entity.EntityRendererProvider.Context
import net.minecraft.client.renderer.entity.LivingEntityRenderer
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.phys.Vec3
import kotlin.math.min
class NPCRenderer(context: Context) : LivingEntityRenderer<NPCEntity, PosableEntityModel<NPCEntity>>(context, PosableNPCModel(), 0.5f) {
@@ -26,6 +28,8 @@ class NPCRenderer(context: Context) : LivingEntityRenderer<NPCEntity, PosableEnt
return VaryingModelRepository.getTexture(entity.npc.resourceIdentifier, (entity.delegate as NPCClientDelegate))
}
private val heldItemRenderer = HeldItemRenderer()
override fun render(
entity: NPCEntity,
entityYaw: Float,
@@ -56,6 +60,20 @@ class NPCRenderer(context: Context) : LivingEntityRenderer<NPCEntity, PosableEnt
model.blue = 1F
model.resetLayerContext()
if (entity.deathTime < 1){
//Render Held Item
heldItemRenderer.renderOnEntity(
entity,
entity.mainHandItem,
model,
clientDelegate,
poseMatrix,
buffer,
packedLight,
Vec3(-90.0,0.0,0.0)
)
}
// if (this.shouldRenderLabel(entity)) {
// this.renderLabelIfPresent(entity, entity.displayName, poseMatrix, buffer, packedLight)
// }
@@ -101,7 +101,7 @@ class PokemonRenderer(
packedLight: Int
) {
val clientDelegate = entity.delegate as PokemonClientDelegate
shadowRadius = min((entity.boundingBox.maxX - entity.boundingBox.minX), (entity.boundingBox.maxZ) - (entity.boundingBox.minZ)).toFloat() / 1.5F * (entity.delegate as PokemonClientDelegate).entityScaleModifier
shadowRadius = (min((entity.boundingBox.maxX - entity.boundingBox.minX), (entity.boundingBox.maxZ) - (entity.boundingBox.minZ)).toFloat() / 1.5F * (entity.delegate as PokemonClientDelegate).entityScaleModifier)/entity.scale
model.posableModel = VaryingModelRepository.getPoser(entity.pokemon.species.resourceIdentifier, clientDelegate)
model.posableModel.context = model.context
model.setupEntityTypeContext(entity)
@@ -160,11 +160,13 @@ class PokemonRenderer(
//Render Held Item
heldItemRenderer.renderOnEntity(
entity,
clientDelegate,
entity.shownItem,
model.posableModel,
clientDelegate,
poseMatrix,
buffer,
packedLight
packedLight,
Vec3(0.0,-90.0,90.0)
)
}
@@ -222,8 +224,14 @@ class PokemonRenderer(
val phaseTarget = clientDelegate.phaseTarget ?: return
poseMatrix.pushPose()
var beamSourcePosition = if (phaseTarget is PosableEntity) {
(phaseTarget.delegate as PosableState).locatorStates["beam"]?.getOrigin() ?: phaseTarget.position()
var beamSourcePosition = if (phaseTarget is NPCEntity) {
val npcDelegate = phaseTarget.delegate as NPCClientDelegate
val baseScale = phaseTarget.baseScale?.toDouble() ?: 1.0
(npcDelegate.locatorStates["beam"]?.getOrigin()?.scale(baseScale))
?: phaseTarget.position().add(0.0, (phaseTarget.bbHeight / 2.0) * baseScale, 0.0)
} else if (phaseTarget is PosableEntity) {
(phaseTarget.delegate as PosableState).locatorStates["beam"]?.getOrigin() ?: phaseTarget.position()
} else {
if (phaseTarget.uuid == Minecraft.getInstance().player?.uuid) {
val lookVec = phaseTarget.lookAngle.yRot((PI / 2).toFloat()).multiply(1.0, 0.0, 1.0).normalize()
@@ -303,7 +311,11 @@ class PokemonRenderer(
val lookVec = beamTarget.lookAngle.yRot((PI / 2).toFloat()).multiply(1.0, 0.0, 1.0).normalize()
beamTarget.getEyePosition(partialTicks).subtract(0.0, 0.4, 0.0).subtract(lookVec.scale(0.3))
} else if (beamTarget is NPCEntity) {
(beamTarget.delegate as NPCClientDelegate).locatorStates["beam"]?.getOrigin() ?: beamTarget.position()
val npcDelegate = beamTarget.delegate as NPCClientDelegate
val baseScale = beamTarget.baseScale?.toDouble() ?: 1.0
(npcDelegate.locatorStates["beam"]?.getOrigin()?.scale(baseScale))
?: beamTarget.position().add(0.0, (beamTarget.bbHeight / 2.0) * baseScale, 0.0)
} else {
val lookVec = beamTarget.lookAngle.yRot((PI / 2 - (beamTarget.visualRotationYInDegrees - beamTarget.xRot).toRadians()).toFloat()).multiply(1.0, 0.0, 1.0).normalize()
beamTarget.getEyePosition(partialTicks).subtract(0.0, 0.7, 0.0).subtract(lookVec.scale(0.4))
@@ -1,114 +0,0 @@
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.util.permission
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.context.CommandContext
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.server.level.ServerPlayer
import net.minecraft.network.chat.Component
import net.minecraft.server.MinecraftServer
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import java.util.LinkedList
object AlterShinyChanceCommand {
private var currentTask: Runnable? = null
private var remainingTime: Long = 0
private var shinyRate: Float = config.shinyRate
private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
private val queue: LinkedList<Pair<Float, Long>> = LinkedList()
fun register(dispatcher: CommandDispatcher<CommandSourceStack>) {
dispatcher.register(
Commands.literal("altershinychance")
.permission(CobblemonPermissions.ALTER_SHINY_CHANCE)
.then(
Commands.argument("rate", IntegerArgumentType.integer(1))
.executes { execute(it, it.source.server, it.source.playerOrException, null) }
.then(
Commands.argument("duration", IntegerArgumentType.integer(1))
.requires { it.player != null }
.executes { execute(it, it.source.server, it.source.playerOrException, IntegerArgumentType.getInteger(it, "duration").toLong()) }
)
)
.then(
Commands.literal("skip")
.requires { it.player != null }
.executes { skipNext(it, it.source.server, it.source.playerOrException) }
)
.then(
Commands.literal("clear")
.requires { it.player != null }
.executes { clearQueue(it, it.source.server, it.source.playerOrException) }
)
)
}
private fun execute(context: CommandContext<CommandSourceStack>, server: MinecraftServer, player: ServerPlayer, duration: Long?): Int {
val rate = IntegerArgumentType.getInteger(context, "rate").toFloat()
if (duration == null) {
// Permanent change
config.shinyRate = rate
queue.clear()
player.sendSystemMessage(Component.literal("Shiny rate permanently set to 1 in ${rate.toInt()}"))
} else {
queue.add(Pair(rate, duration))
if (currentTask == null) {
startNextShinyRate(server, player)
} else {
player.sendSystemMessage(Component.literal("Shiny rate queued to 1 in ${rate.toInt()} for $duration seconds."))
}
}
return Command.SINGLE_SUCCESS
}
private fun startNextShinyRate(server: MinecraftServer, player: ServerPlayer) {
if (queue.isEmpty()) {
config.shinyRate = shinyRate // Reset to original
currentTask = null
player.sendSystemMessage(Component.literal("Shiny rate has returned to 1 in ${shinyRate.toInt()}"))
return
}
val (rate, duration) = queue.poll()
config.shinyRate = rate
remainingTime = duration
player.sendSystemMessage(Component.literal("Shiny rate set to 1 in ${rate.toInt()} for $duration seconds."))
currentTask = Runnable {
if (remainingTime > 0) {
remainingTime -= 10
scheduler.schedule(currentTask, 10, TimeUnit.SECONDS)
} else {
startNextShinyRate(server, player)
}
}
scheduler.schedule(currentTask, 10, TimeUnit.SECONDS)
}
private fun skipNext(context: CommandContext<CommandSourceStack>, server: MinecraftServer, player: ServerPlayer): Int {
if (queue.isNotEmpty()) {
player.sendSystemMessage(Component.literal("Skipping current shiny rate duration."))
startNextShinyRate(server, player)
} else {
player.sendSystemMessage(Component.literal("No active shiny rate durations to skip."))
}
return Command.SINGLE_SUCCESS
}
private fun clearQueue(context: CommandContext<CommandSourceStack>, server: MinecraftServer, player: ServerPlayer): Int {
queue.clear()
remainingTime = 0
config.shinyRate = shinyRate
currentTask = null
player.sendSystemMessage(Component.literal("All shiny rate durations cleared. Shiny rate reset to 1 in ${shinyRate.toInt()}"))
return Command.SINGLE_SUCCESS
}
}
@@ -1,41 +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.command
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
object ReloadShowdownCommand {
fun register(dispatcher: CommandDispatcher<CommandSourceStack>) {
val command = Commands.literal("reloadshowdown")
.requires { it.hasPermission(4) }
.executes(::execute)
dispatcher.register(command)
}
private fun execute(context: CommandContext<CommandSourceStack>): Int {
try {
ShowdownService.service.closeConnection()
ShowdownService.service.openConnection()
ShowdownService.service.registerSpecies()
ShowdownService.service.registerBagItems()
context.source.sendSystemMessage(Component.literal("Reloaded showdown"))
} catch (e: Exception) {
e.printStackTrace()
}
return Command.SINGLE_SUCCESS
}
}
@@ -13,7 +13,6 @@ import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.command.argument.NPCClassArgumentType
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.alias
import com.cobblemon.mod.common.util.commandLang
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.toBlockPos
import com.mojang.brigadier.Command
@@ -84,4 +83,4 @@ object SpawnNPCCommand {
}
throw FAILED_SPAWN_EXCEPTION.create()
}
}
}
@@ -23,7 +23,7 @@ import com.cobblemon.mod.common.util.effectiveName
import com.cobblemon.mod.common.util.update
import java.util.concurrent.CompletableFuture
class NPCBattleActor(
open class NPCBattleActor(
val npc: NPCEntity,
pokemonList: List<BattlePokemon>,
val skill: Int
@@ -84,4 +84,4 @@ class NPCBattleActor(
// }
// }
}
}
}
@@ -557,10 +557,10 @@ class NPCEntity(world: Level) : AgeableMob(CobblemonEntities.NPC, world), Npc, P
private fun pokemonExpressions(pokemon: Pokemon): List<String> {
return listOf(
"v.actioning_pokemon_name=\"${pokemon.species.name}\";",
"v.actioning_pokemon_name=\'${pokemon.species.name}\';",
"v.actioning_pokemon_level=${pokemon.level};",
"v.actioning_pokemon_ball=\"${pokemon.caughtBall.name}\";",
"v.actioning_pokemon_shiny=\"${pokemon.shiny}\";",
"v.actioning_pokemon_ball=\'${pokemon.caughtBall.name}\';",
"v.actioning_pokemon_shiny=\'${pokemon.shiny}\';",
)
}
@@ -959,6 +959,7 @@ open class PokemonEntity(
val scale = effects.mockEffect?.scale ?: (form.baseScale * pokemon.scaleModifier)
var result = this.exposedForm.hitbox.scale(scale)
result = result.withEyeHeight(this.exposedForm.eyeHeight(this) * result.height)
result = result.scale(this.scale)
return result
}
@@ -1310,7 +1311,7 @@ open class PokemonEntity(
}
} else if (form.behaviour.moving.swim.canBreatheUnderwater && !form.behaviour.moving.swim.canWalkOnWater) {
// Use half hitbox height for swimmers
val halfHeight = form.hitbox.height * form.baseScale / 2.0
val halfHeight = getDimensions(this.pose).height / 2.0
for (i in 1..halfHeight.toInt()) {
blockPos = blockPos.below()
if (!this.level().isWaterAt(blockPos) || !this.level().getBlockState(blockPos).getCollisionShape(this.level(), blockPos).isEmpty) {
@@ -1812,6 +1813,11 @@ open class PokemonEntity(
return this.riding.speed(this, controller)
}
fun useRidingAltPose(): Boolean {
val driver = this.controllingPassenger as? Player ?: return false
return this.riding.useRidingAltPose(this, driver)
}
var jumpInputStrength: Int = 0 // move this
override fun onPlayerJump(strength: Int) {
// See if this controls the hot bar element
@@ -0,0 +1,68 @@
package com.cobblemon.mod.common.net.messages.client.debug.showdown
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import java.util.Date
/**
* Packet sent to refresh the showdown information with the current status, messages, and available services.
* This is typically sent when a user is in the Showdown Debug UI after they send a [RequestShowdownInfoPacket].
*
* @property status The current status of the showdown service.
* @property service The resource location of the showdown service.
* @property messages A list of messages from the active showdown service currently being refreshed.
* @property availableServices A list of available showdown services. This allows the client to switch which server they are viewing.
* @property requestProcessedAt The date and time when the request was processed.
* @property includesStartOfLog Indicates if the messages include the start of the log.
*
* @author landonjw
*/
class ShowdownRefreshInfoPacket(
val status: ShowdownStatus,
val service: ResourceLocation,
val messages: List<String>,
val availableServices: List<ResourceLocation>,
val requestProcessedAt: Date,
val includesStartOfLog: Boolean
) : NetworkPacket<ShowdownRefreshInfoPacket> {
companion object {
val ID = cobblemonResource("refresh_showdown_info")
/**
* Decodes a [ShowdownRefreshInfoPacket] from the given [RegistryFriendlyByteBuf].
*
* @param buffer The buffer to read from.
* @return The decoded [ShowdownRefreshInfoPacket].
*/
fun decode(buffer: RegistryFriendlyByteBuf) = ShowdownRefreshInfoPacket(
status = ShowdownStatus.valueOf(buffer.readString()),
service = buffer.readResourceLocation(),
messages = buffer.readList { buffer.readString() },
availableServices = buffer.readList { buffer.readResourceLocation() },
requestProcessedAt = buffer.readDate(),
includesStartOfLog = buffer.readBoolean()
)
}
override val id = ID
/**
* Encodes this [ShowdownRefreshInfoPacket] to the given [RegistryFriendlyByteBuf].
*
* @param buffer The buffer to write to.
*/
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeString(status.name)
buffer.writeResourceLocation(service)
buffer.writeCollection(messages) { pb, value -> pb.writeString(value) }
buffer.writeCollection(availableServices) { pb, value -> pb.writeResourceLocation(value) }
buffer.writeDate(requestProcessedAt)
buffer.writeBoolean(includesStartOfLog)
}
}
@@ -0,0 +1,42 @@
package com.cobblemon.mod.common.net.messages.server.debug.showdown
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import java.util.*
/**
* Packet sent to request showdown information for a specific server.
*
* @property service The id of the showdown service to get showdown information for.
* @author landonjw
*/
class RequestShowdownInfoPacket(val service: ResourceLocation, val lastRequest: Date?) : NetworkPacket<RequestShowdownInfoPacket> {
override val id = ID
/**
* Encodes this [RequestShowdownInfoPacket] to the given [RegistryFriendlyByteBuf].
*
* @param buffer The buffer to write to.
*/
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(service)
buffer.writeNullable(lastRequest) { pb, value -> pb.writeDate(value) }
}
companion object {
val ID = cobblemonResource("request_showdown_info")
/**
* Decodes a [RequestShowdownInfoPacket] from the given [RegistryFriendlyByteBuf].
*
* @param buffer The buffer to read from.
* @return The decoded [RequestShowdownInfoPacket].
*/
fun decode(buffer: RegistryFriendlyByteBuf) = RequestShowdownInfoPacket(
service = buffer.readResourceLocation(),
lastRequest = buffer.readNullable { pb -> pb.readDate() }
)
}
}
@@ -0,0 +1,49 @@
package com.cobblemon.mod.common.net.messages.server.debug.showdown
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* A network packet for sending action commands to the Showdown service.
*
* @property service The resource location of the Showdown service.
* @property action The action command to be sent to the Showdown service.
* @author landonjw
*/
class ShowdownActionPacket(val service: ResourceLocation, val action: Action) : NetworkPacket<ShowdownActionPacket> {
override val id = ID
/**
* Encodes the packet data into the given buffer.
*
* @param buffer The buffer to encode the data into.
*/
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(service)
buffer.writeEnum(action)
}
companion object {
val ID = cobblemonResource("showdown_action_request")
/**
* Decodes the packet data from the given buffer.
*
* @param buffer The buffer to decode the data from.
* @return The decoded ShowdownActionPacket.
*/
fun decode(buffer: RegistryFriendlyByteBuf) = ShowdownActionPacket(
service = buffer.readResourceLocation(),
action = buffer.readEnum(Action::class.java)
)
}
/**
* Enum representing possible actions for the Showdown service.
*/
enum class Action {
START, STOP, RESTART, QUEUE
}
}
@@ -0,0 +1,44 @@
package com.cobblemon.mod.common.net.messages.server.debug.showdown
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* A network packet for sending input commands to the Showdown service.
*
* @property service The resource location of the Showdown service.
* @property input The input command to be sent to the Showdown service.
* @author landonjw
*/
class ShowdownInputPacket(val service: ResourceLocation, val input: String) : NetworkPacket<ShowdownInputPacket> {
override val id = ID
/**
* Encodes the packet data into the given buffer.
*
* @param buffer The buffer to encode the data into.
*/
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(service)
buffer.writeString(input)
}
companion object {
val ID = cobblemonResource("showdown_info_request")
/**
* Decodes the packet data from the given buffer.
*
* @param buffer The buffer to decode the data from.
* @return The decoded ShowdownInputPacket.
*/
fun decode(buffer: RegistryFriendlyByteBuf) = ShowdownInputPacket(
service = buffer.readResourceLocation(),
input = buffer.readString()
)
}
}
@@ -0,0 +1,40 @@
package com.cobblemon.mod.common.net.serverhandling.debug.showdown
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.RequestShowdownInfoPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import java.util.Date
/**
* Handles the reception of a [RequestShowdownInfoPacket] on the server side.
* This handler retrieves the showdown information from the specified service
* and sends it back to the client.
*
* @author landonjw
*/
object RequestShowdownInfoHandler : ServerNetworkPacketHandler<RequestShowdownInfoPacket> {
/**
* Handles the received [RequestShowdownInfoPacket] by retrieving the showdown information
* from the specified service and sending it back to the client in a [ShowdownRefreshInfoPacket].
*
* @param packet The received packet requesting showdown information.
* @param server The Minecraft server instance.
* @param player The player who sent the request.
*/
override fun handle(packet: RequestShowdownInfoPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!ShowdownService.contains(packet.service)) return
val service = ShowdownService.get(packet.service)
val messages = service.log.get(after = packet.lastRequest)
val availableServices = ShowdownService.all().map { it.id }
val includesStartOfLog = if (packet.lastRequest == null) true else service.log.isStartOfLog(messages)
player.sendPacket(ShowdownRefreshInfoPacket(service.status, packet.service, messages, availableServices, Date(), includesStartOfLog))
}
}
@@ -0,0 +1,53 @@
package com.cobblemon.mod.common.net.serverhandling.debug.showdown
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.battles.BattleRegistry.packTeam
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownActionPacket
import com.cobblemon.mod.common.util.party
import com.google.gson.JsonObject
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
/**
* Handles network packets for Showdown actions on the server side.
*
* This handler processes incoming `ShowdownActionPacket` packets and performs the corresponding
* actions (ie. restart it) on the specified Showdown service.
*
* @author landonjw
*/
object ShowdownActionHandler : ServerNetworkPacketHandler<ShowdownActionPacket> {
/**
* Handles the received `ShowdownActionPacket`.
*
* @param packet The received packet containing the action to perform.
* @param server The Minecraft server instance.
* @param player The player who sent the packet.
*/
override fun handle(packet: ShowdownActionPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!ShowdownService.contains(packet.service)) return
val service = ShowdownService.get(packet.service)
when (packet.action) {
ShowdownActionPacket.Action.START -> service.openConnection()
ShowdownActionPacket.Action.STOP -> service.closeConnection()
ShowdownActionPacket.Action.RESTART -> service.restartConnection()
ShowdownActionPacket.Action.QUEUE -> {
val cobblemonTeam = player.party().saveToJSON(JsonObject(), player.registryAccess())
val showdownTeam = player.party().map { BattlePokemon(it, it.clone(false)) { entity -> entity.discard() } }
showdownTeam.forEach {
it.effectedPokemon.level = 100
it.effectedPokemon.heal()
}
val showdownTeamStr = showdownTeam.packTeam()
service.send(">queue-battle ${player.uuid} ${player.name.string} $cobblemonTeam@|@$showdownTeamStr")
}
}
}
}
@@ -0,0 +1,33 @@
package com.cobblemon.mod.common.net.serverhandling.debug.showdown
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownInputPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
/**
* Handles the processing of `ShowdownInputPacket` on the server side.
* This handler sends a command to the given showdown service. This is used for debugging purposes.
*
* @author landonjw
*/
object ShowdownInputHandler : ServerNetworkPacketHandler<ShowdownInputPacket> {
/**
* Handles the received `ShowdownInputPacket`.
*
* @param packet The received packet containing the input command and service information.
* @param server The Minecraft server instance.
* @param player The player who sent the packet.
*/
override fun handle(packet: ShowdownInputPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!ShowdownService.contains(packet.service)) return
val service = ShowdownService.get(packet.service)
service.send(packet.input)
}
}
@@ -1069,14 +1069,14 @@ open class Pokemon : ShowdownIdentifiable {
this.experience = other.experience
this.setFriendship(other.friendship)
// Applied before current health for calcs to take place
other.ivs.doWithoutEmitting {
this.ivs.forEach {
other.ivs[it.key] = it.value
this.ivs.doWithoutEmitting {
other.ivs.forEach {
this.ivs[it.key] = it.value
}
}
other.evs.doWithoutEmitting {
this.evs.forEach {
other.evs[it.key] = it.value
this.evs.doWithoutEmitting {
other.evs.forEach {
this.evs[it.key] = it.value
}
}
this.currentHealth = other.currentHealth
@@ -1105,7 +1105,9 @@ open class Pokemon : ShowdownIdentifiable {
this.originalTrainerType = other.originalTrainerType
this.originalTrainer = other.originalTrainer
this.forcedAspects = other.forcedAspects
this.features = other.features
this.cosmeticItem = other.cosmeticItem
this.updateAspects()
this.refreshOriginalTrainer()
this.initialize()
return this
@@ -32,7 +32,7 @@ abstract class BaseCobblemonHeldItemManager : HeldItemManager {
internal open fun load() {
this.itemIds.clear()
val itemsJson = ShowdownService.service.getItemIds()
val itemsJson = ShowdownService.get().getItemIds()
val showdownIds = hashSetOf<String>()
for (i in 0 until itemsJson.size()) {
showdownIds += itemsJson[i].asString
@@ -95,4 +95,4 @@ abstract class BaseCobblemonHeldItemManager : HeldItemManager {
}
return null
}
}
}
@@ -9,7 +9,7 @@
package com.cobblemon.mod.common.pokemon.riding.controllers
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.MoLangMath.lerp
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Rollable
import com.cobblemon.mod.common.api.riding.controller.RideController
@@ -19,7 +19,9 @@ import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.riding.states.BirdAirState
import com.cobblemon.mod.common.util.*
import net.minecraft.client.Minecraft
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.util.Mth
import net.minecraft.util.SmoothDouble
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
@@ -32,81 +34,37 @@ class BirdAirController : RideController {
override val poseProvider = PoseProvider(PoseType.HOVER)
.with(PoseOption(PoseType.FLY) {
val player = it.passengers.firstOrNull() as? Player ?: return@PoseOption false
it.deltaMovement.length() > 0.3 })
/*
.with(PoseOption(PoseType.FLY) {
val player = it.passengers.firstOrNull() as? Player ?: return@PoseOption false
player.zza > 0.0f // Checks if the player is actively attempting to move forward
})
*/
it.deltaMovement.length() > 0.5 })
override val condition: (PokemonEntity) -> Boolean = { true }
var jumpVector = listOf("0".asExpression(), "0.3".asExpression(), "0".asExpression())
var handlingExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 135.0, 45.0)".asExpression()
private set
var gravity: Expression = "0".asExpression()
var topSpeedExpr: Expression = "q.get_ride_stats('SPEED', 'AIR', 1.0, 0.35)".asExpression()
private set
var horizontalAcceleration: Expression = "0.1".asExpression()
var glideTopSpeedExpr: Expression = "q.get_ride_stats('SPEED', 'AIR', 2.0, 1.0)".asExpression()
private set
var verticalAcceleration: Expression = "0.1".asExpression()
// Max accel is a whole 1.0 in 3 seconds. The conversion in the function below is to convert seconds to ticks
var accelExpr: Expression = "q.get_ride_stats('ACCELERATION', 'AIR', (1.0 / (20.0 * 3.0)), (1.0 / (20.0 * 8.0)))".asExpression()
private set
//Seconds self propelled flight (glide is not self propelled in this case)
var staminaExpr: Expression = "q.get_ride_stats('STAMINA', 'AIR', 120.0, 20.0)".asExpression()
private set
//max y level for the ride
var altitudeExpr: Expression = "q.get_ride_stats('JUMP', 'AIR', 200.0, 128.0)".asExpression()
private set
// Where 90 is the width of the range and 45 is the offset.
// So the handling interpolates between 135 and 45
var handlingExpr: Expression = "(q.get_ride_stats('SKILL', 'AIR') / 100) * 90 + 45".asExpression()
var infiniteStamina: Expression = "false".asExpression()
private set
var topSpeedExpr: Expression = "(q.get_ride_stats('SPEED', 'AIR') / 100) * (1.0 - 0.35) + 0.35".asExpression()
private set
//Reaches max top speed from a standstill at minimum 8 seconds and max 3 seconds.
//This is a bit of a confusing expression and likely should be cleaned up to be more readable
var accelExpr: Expression = "(q.get_ride_stats('ACCELERATION', 'AIR') / 100) * (0.01666666666 - 0.00625) + 0.00625".asExpression()
var infiniteAltitude: Expression = "false".asExpression()
private set
override fun speed(entity: PokemonEntity, driver: Player): Float {
//Retrieve the current bird controller state
//val state = entity.riding.getState(KEY) { e -> BirdAirControllerState(e) }
//Retrieve the current bird controller state and stats
val state = getState(entity, ::BirdAirState)
//retrieve stats
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val accel = getRuntime(entity).resolveDouble(accelExpr)
val maxGlideSpeed = topSpeed * 2
var glideSpeedChange = 0.00
//speed up and slow down based on input
if (driver.zza > 0.0 && state.currSpeed < topSpeed) {
state.currSpeed = min(state.currSpeed + accel , topSpeed)
} else if (driver.zza < 0.0 && state.currSpeed > 0.0) {
//Decelerate is currently always a constant half of max acceleration.
state.currSpeed = max(state.currSpeed - (0.0166 / 2), 0.0)
}
val rollable = driver as? Rollable
if (rollable != null) {
glideSpeedChange = sin(Math.toRadians(rollable.pitch.toDouble()))
glideSpeedChange = glideSpeedChange.pow(3)
//TODO: Possibly create a deadzone around parallel where speed isn't taken away from?
if (glideSpeedChange <= 0.0) {
//Ensures that a propelling force is still able to be applied when
//climbing in height
if (driver.zza <= 0) {
//speed decrease should be 2x speed increase?
state.currSpeed = max(state.currSpeed + (0.0166) * glideSpeedChange, 0.0 )
}
} else {
// only add to the speed if it hasn't exceeded the current
//glide angles maximum amount of speed that it can give.
state.currSpeed = min(state.currSpeed + ((0.0166 * 2) * glideSpeedChange), maxGlideSpeed)
}
}
//air resistance
state.currSpeed = max(state.currSpeed - 0.005, 0.0)
return state.currSpeed.toFloat()
return state.rideVel.length().toFloat()
}
override fun rotation(entity: PokemonEntity, driver: LivingEntity): Vec2 {
@@ -118,62 +76,60 @@ class BirdAirController : RideController {
var leftForce = 0.0
var upForce = 0.0
var forwardForce = 0.0
val state = getState(entity, ::BirdAirState)
//Perform ride velocity update
calculateRideSpaceVel(entity, driver, state)
//Translate ride space velocity to world space velocity.
val rollable = driver as? Rollable
if (rollable != null) {
//Need to deadzone this when straight up or down
// test with and without strafing added. May be wanted while moving
leftForce = -1.0 * sin(Math.toRadians(rollable.roll.toDouble()))
upForce = -1.0 * sin(Math.toRadians(rollable.pitch.toDouble()))
upForce += -1.0 * sin(Math.toRadians(rollable.pitch.toDouble())) * state.rideVel.z
forwardForce += cos(Math.toRadians(rollable.pitch.toDouble())) * state.rideVel.z
forwardForce = cos(Math.toRadians(rollable.pitch.toDouble()))
upForce += cos(Math.toRadians(rollable.pitch.toDouble())) * state.rideVel.y
forwardForce += sin(Math.toRadians(rollable.pitch.toDouble())) * state.rideVel.y
}
//TODO: Add in an upwards and downwards force when pressing space maybe?
//Leave out tap to flap right now
/*
var jumpVector = Vec3.ZERO
if (entity.jumpInputStrength > 0) {
val runtime = getRuntime(entity)
runtime.environment.query.addFunction("jump_strength") { DoubleValue(entity.jumpInputStrength.toDouble()) }
val jumpForces = this.jumpVector.map { runtime.resolveFloat(it) }
jumpVector = Vec3(jumpForces[0].toDouble(), jumpForces[1].toDouble(), jumpForces[2].toDouble())
jumpVector = jumpVector.toVector3f().mul((driver as Rollable).orientation).toVec3d()
entity.jumpInputStrength = 0
entity.addDeltaMovement(jumpVector)
//Bring the ride out of the sky when stamina is depleted.
if (state.stamina <= 0.0) {
upForce -= 0.3
}
*/
//Retrieve the current composite controller state
val state = getState(entity, ::BirdAirState)
val velocity = Vec3(0.0 , upForce * state.currSpeed, forwardForce * state.currSpeed)
val altitudeLimit = getRuntime(entity).resolveDouble(altitudeExpr)
//Only limit altitude if altitude is not infinite
if (!getRuntime(entity).resolveBoolean(infiniteAltitude)) {
//Provide a hard limit on altitude
upForce = if (entity.y >= altitudeLimit && upForce > 0) 0.0 else upForce
}
val velocity = Vec3(state.rideVel.x , upForce, forwardForce)
return velocity
}
override fun angRollVel(entity: PokemonEntity, driver: Player, deltaTime: Double): Vec3 {
val rollable = driver as? Rollable
val state = getState(entity, ::BirdAirState)
//TODO: Tie in handling
val handling = getRuntime(entity).resolveDouble(handlingExpr)
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val rotationChangeRate = 10.0
if (rollable != null) {
var yawForce = rotationChangeRate * sin(Math.toRadians(rollable.roll.toDouble()))
//for a bit of correction on the rolls limit it to a quarter the amount
var pitchForce = -0.25 * rotationChangeRate * abs(sin(Math.toRadians(rollable.roll.toDouble())))
//for a bit of correction on the rolls influence pitch as well
var pitchForce = -0.35 * rotationChangeRate * abs(sin(Math.toRadians(rollable.roll.toDouble())))
//limit rotation modulation when pitched up heavily or pitched down heavily
yawForce *= abs(cos(Math.toRadians(rollable.pitch.toDouble())))
pitchForce *= abs(cos(Math.toRadians(rollable.pitch.toDouble()))) * 1.5
//Create a deadzone for easier leveling out
if (rollable.roll <= 0.0 && rollable.roll >= -0.0) {
yawForce = 0.0
pitchForce = 0.0
}
return Vec3(yawForce, pitchForce, 0.0)
}
@@ -191,42 +147,56 @@ class BirdAirController : RideController {
deltaTime: Double
): Vec3 {
if (driver !is Rollable) return Vec3.ZERO
val rollable = driver as Rollable
val state = getState(entity, ::BirdAirState)
val handling = getRuntime(entity).resolveDouble(handlingExpr)
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
//Smooth out mouse input.
val smoothingSpeed = 4
val invertRoll = if (Cobblemon.config.invertRoll) -1 else 1
val invertPitch = if (Cobblemon.config.invertPitch) -1 else 1
var xInput = xMouseSmoother.getNewDeltaValue(xMouse * 0.1 * invertRoll, deltaTime * smoothingSpeed);
val YInput = yMouseSmoother.getNewDeltaValue(yMouse * 0.1 * invertPitch, deltaTime * smoothingSpeed);
val xInput = xMouseSmoother.getNewDeltaValue(xMouse * 0.1 * invertRoll, deltaTime * smoothingSpeed);
val yInput = yMouseSmoother.getNewDeltaValue(yMouse * 0.1 * invertPitch, deltaTime * smoothingSpeed);
//limit rolling based on healing
val rotLimit = handling
//limit rolling based on handling and current speed.
//modulated by speed so that when flapping idle in air you are ont wobbling around to look around
val rotMin = 15.0
var rollForce = xInput
val rotLimit = max(handling * normalizeVal(state.rideVel.length(), 0.0, topSpeed).pow(3), rotMin)
//Limit roll by non linearly decreasing inputs towards
// a rotation limit based on the current distance from
// that rotation limit
if (abs(rollable.roll + xInput) < rotLimit) {
if (sign(xInput) == sign(rollable.roll).toDouble()) {
if (abs(rollable.roll + rollForce) < rotLimit) {
if (sign(rollForce) == sign(rollable.roll).toDouble()) {
val d = abs(abs(rollable.roll) - rotLimit)
xInput *= (d.pow(2)) / (rotLimit.pow(2))
rollForce *= (d.pow(2)) / (rotLimit.pow(2))
}
} else if (sign(xInput) == sign(rollable.roll).toDouble()) {
xInput = 0.0
} else if (sign(rollForce) == sign(rollable.roll).toDouble()) {
rollForce = 0.0
}
//Give the ability to yaw with x mouse input when at low speeds.
val yawForce = xInput * ( 1.0 - normalizeVal(state.rideVel.length(), 0.0, topSpeed).pow(3))
//yaw, pitch, roll
return Vec3(0.0, YInput, xInput)
return Vec3(yawForce, yInput, rollForce)
}
override fun canJump(
entity: PokemonEntity,
driver: Player
): Boolean {
return true
return false
}
override fun setRideBar(entity: PokemonEntity, driver: Player): Float {
//Retrieve stamina from state
val state = getState(entity, ::BirdAirState)
return (state.stamina / 1.0f)
}
override fun jumpForce(
@@ -243,29 +213,190 @@ class BirdAirController : RideController {
override fun shouldRoll(entity: PokemonEntity) = true
override fun rideFovMult(entity: PokemonEntity, driver: Player): Float {
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val glideTopSpeed = getRuntime(entity).resolveDouble(glideTopSpeedExpr)
val state = getState(entity, ::BirdAirState)
//Must I ensure that topspeed is greater than minimum?
val normalizedGlideSpeed = normalizeVal(state.rideVel.length(), topSpeed, glideTopSpeed)
//Only ever want the fov change to be a max of 0.2 and for it to have non linear scaling.
return 1.0f + normalizedGlideSpeed.pow(2).toFloat() * 0.2f
}
override fun useAngVelSmoothing(entity: PokemonEntity): Boolean = false
override fun useRidingAltPose(entity: PokemonEntity, driver: Player): Boolean {
val state = getState(entity, ::BirdAirState)
return state.gliding
}
override fun encode(buffer: RegistryFriendlyByteBuf) {
super.encode(buffer)
buffer.writeString(gravity.toString())
buffer.writeString(horizontalAcceleration.toString())
buffer.writeString(verticalAcceleration.toString())
buffer.writeString(topSpeedExpr.toString())
buffer.writeString(glideTopSpeedExpr.toString())
buffer.writeString(accelExpr.toString())
buffer.writeString(handlingExpr.toString())
buffer.writeString(altitudeExpr.toString())
buffer.writeString(infiniteStamina.toString())
buffer.writeString(infiniteAltitude.toString())
}
override fun decode(buffer: RegistryFriendlyByteBuf) {
gravity = buffer.readString().asExpression()
horizontalAcceleration = buffer.readString().asExpression()
verticalAcceleration = buffer.readString().asExpression()
topSpeedExpr = buffer.readString().asExpression()
glideTopSpeedExpr = buffer.readString().asExpression()
accelExpr = buffer.readString().asExpression()
handlingExpr = buffer.readString().asExpression()
altitudeExpr = buffer.readString().asExpression()
infiniteStamina = buffer.readString().asExpression()
infiniteAltitude = buffer.readString().asExpression()
}
companion object {
val KEY = cobblemonResource("air/bird")
}
/*
* Normalizes the given value between a min and a max.
* The result is clamped between 0.0 and 1.0, where 0.0 represents x is at or below min
* and 1.0 represents x is at or above it.
*/
fun normalizeVal(x: Double, min: Double, max: Double): Double {
require(max > min) { "max must be greater than min" }
return ((x - min) / (max - min)).coerceIn(0.0, 1.0)
}
/*
* Calculates the change in the ride space vector due to player input and ride state
*/
fun calculateRideSpaceVel(entity: PokemonEntity, driver: Player, state: BirdAirState) {
//retrieve stats
val topSpeed = getRuntime(entity).resolveDouble(topSpeedExpr)
val glideTopSpeed = getRuntime(entity).resolveDouble(glideTopSpeedExpr)
val accel = getRuntime(entity).resolveDouble(accelExpr)
val staminaStat = getRuntime(entity).resolveDouble(staminaExpr)
var glideSpeedChange = 0.0
val currSpeed = state.rideVel.length()
//Flag for determining if player is actively inputting
var activeInput = false
//speed up and slow down based on input
if (driver.zza != 0.0f && state.stamina > 0.0) {
//make sure it can't exceed top speed
val forwardInput = when {
driver.zza > 0 && state.rideVel.z > topSpeed -> 0.0
driver.zza < 0 && state.rideVel.z < (-topSpeed / 3.0) -> 0.0
else -> driver.zza.sign
}
state.rideVel = Vec3(
state.rideVel.x,
state.rideVel.y,
(state.rideVel.z + (accel * forwardInput.toDouble())))
activeInput = true
}
val rollable = driver as? Rollable
if (rollable != null) {
//Base glide speed change on current pitch of the ride.
glideSpeedChange = sin(Math.toRadians(rollable.pitch.toDouble()))
glideSpeedChange = glideSpeedChange.pow(3) * 0.5
//TODO: Possibly create a deadzone around parallel where glide doesn't affect speed?
if (glideSpeedChange <= 0.0) {
//Ensures that a propelling force is still able to be applied when
//climbing in height
if (driver.zza <= 0) {
//speed decrease should be 2x speed increase?
//state.currSpeed = max(state.currSpeed + (0.0166) * glideSpeedChange, 0.0 )
state.rideVel = Vec3(
state.rideVel.x,
state.rideVel.y,
lerp( state.rideVel.z, 0.0,glideSpeedChange * -0.0166 * 2 ))
}
} else {
// only add to the speed if it hasn't exceeded the current
//glide angles maximum amount of speed that it can give.
//state.currSpeed = min(state.currSpeed + ((0.0166 * 2) * glideSpeedChange), maxGlideSpeed)
state.rideVel = Vec3(
state.rideVel.x,
state.rideVel.y,
min(state.rideVel.z + ((0.0166 * 2) * glideSpeedChange), glideTopSpeed))
}
}
//Lateral movement based on driver input.
val latTopSpeed = topSpeed / 2.0
if (driver.xxa != 0.0f && state.stamina > 0.0) {
state.rideVel = Vec3(
(state.rideVel.x + (accel * driver.xxa)).coerceIn(-latTopSpeed, latTopSpeed),
state.rideVel.y,
state.rideVel.z)
activeInput = true
}
else {
state.rideVel = Vec3(
lerp(state.rideVel.x, 0.0, latTopSpeed / 20.0),
state.rideVel.y,
state.rideVel.z)
}
//Vertical movement based on driver input.
val vertTopSpeed = topSpeed / 2.0
val vertInput = when {
Minecraft.getInstance().options.keyJump.isDown() -> 1.0
Minecraft.getInstance().options.keyShift.isDown() -> -1.0
else -> 0.0
}
if (vertInput != 0.0 && state.stamina > 0.0) {
state.rideVel = Vec3(
state.rideVel.x,
(state.rideVel.y + (accel * vertInput)).coerceIn(-vertTopSpeed, vertTopSpeed),
state.rideVel.z)
activeInput = true
}
else {
state.rideVel = Vec3(
state.rideVel.x,
lerp(state.rideVel.y, 0.0, vertTopSpeed / 20.0),
state.rideVel.z)
}
//Check if the ride should be gliding
if (activeInput && state.stamina > 0.0) {
state.gliding = false
}else {
state.gliding = true
}
//Only perform stamina logic if the ride does not have infinite stamina
if (!getRuntime(entity).resolveBoolean(infiniteStamina)) {
if (activeInput) {
state.stamina -= (0.05 / staminaStat).toFloat()
}
//Lose a base amount of stamina just for being airborne
state.stamina -= (0.01 / staminaStat).toFloat()
}
else
{
state.stamina = 1.0f
}
//air resistance
state.rideVel = Vec3(
state.rideVel.x,
state.rideVel.y,
lerp( state.rideVel.z,0.0, topSpeed / ( 20.0 * 30.0)))
}
}
@@ -49,9 +49,9 @@ class JetAirController : RideController {
var accelExpr: Expression = "q.get_ride_stats('ACCELERATION', 'AIR', (1.0 / (20.0 * 1.0)), (1.0 / (20.0 * 5.0)))".asExpression()
private set
// Between 60 seconds and 10 seconds at the lowest when at full speed.
var staminaExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 60.0, 10.0)".asExpression()
var staminaExpr: Expression = "q.get_ride_stats('STAMINA', 'AIR', 60.0, 10.0)".asExpression()
private set
var jumpExpr: Expression = "q.get_ride_stats('SKILL', 'AIR', 300.0, 128.0)".asExpression()
var jumpExpr: Expression = "q.get_ride_stats('JUMP', 'AIR', 300.0, 128.0)".asExpression()
private set
// Make configurable by json
@@ -123,14 +123,12 @@ class JetAirController : RideController {
upForce = if(state.stamina > 0.0) upForce else -0.7
val altitudeLimit = getRuntime(entity).resolveDouble(jumpExpr)
val pushingHeightLimit = if(getRuntime(entity).resolveBoolean(infiniteAltitude)) false
else (entity.y >= 128 && entity.xRot <= 0)
//Incorporate the altitude limiter.
//Apply downwards force if limit is reached. Resist upwards force on a buffer of 30 blocks past the limit.
upForce = if(pushingHeightLimit) upForce - upForce * ((entity.y - altitudeLimit) / 30) - 0.5
else upForce
//Only limit altitude if altitude is not infinite
if (!getRuntime(entity).resolveBoolean(infiniteAltitude)) {
//Provide a hard limit on altitude
upForce = if (entity.y >= altitudeLimit && upForce > 0) 0.0 else upForce
}
val velocity = Vec3(0.0 , upForce, forwardForce )
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.util.getString
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.writeString
import net.minecraft.client.Minecraft
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.util.SmoothDouble
import net.minecraft.world.entity.LivingEntity
@@ -71,15 +72,37 @@ class RunUpToFlightCompositeController : RideController {
driver: Player
): Float {
val state = getState(entity, ::RunUpToFlightCompositeState)
if (state.activeController == flightController && entity.onGround() && state.timeTransitioned + 20 < entity.level().gameTime) {
if (state.activeController == flightController && entity.onGround() &&
state.timeTransitioned + 20 < entity.level().gameTime) {
//Pass the speed to the next state
val flightState = flightController.getState(entity, ::BirdAirState)
val groundState = landController.getState(entity, ::GenericLandState)
groundState.currSpeed = flightState.currSpeed
groundState.rideVel = flightState.rideVel
groundState.stamina = flightState.stamina
getState(entity, ::RunUpToFlightCompositeState).activeController = landController
entity.setBehaviourFlag(PokemonBehaviourFlag.FLYING, false)
getState(entity, ::RunUpToFlightCompositeState).let {
it.activeController = landController
entity.setBehaviourFlag(PokemonBehaviourFlag.FLYING, false)
it.timeTransitioned = entity.level().gameTime
}
}
else if (state.activeController == landController && Minecraft.getInstance().options.keyJump.isDown &&
state.timeTransitioned + 20 < entity.level().gameTime) {
//Pass the speed to the next state
val flightState = flightController.getState( entity, ::BirdAirState )
val groundState = landController.getState( entity, ::GenericLandState )
flightState.currSpeed = groundState.currSpeed
flightState.rideVel = groundState.rideVel
flightState.stamina = groundState.stamina
getState(entity, ::RunUpToFlightCompositeState).let {
it.activeController = flightController
entity.setBehaviourFlag(PokemonBehaviourFlag.FLYING, true)
it.timeTransitioned = entity.level().gameTime
}
}
return getActiveController(entity).speed(entity, driver)
@@ -101,6 +124,14 @@ class RunUpToFlightCompositeController : RideController {
return getActiveController(entity).angRollVel(entity, driver, deltaTime)
}
override fun rideFovMult(entity: PokemonEntity, driver: Player): Float {
return getActiveController(entity).rideFovMult(entity, driver)
}
override fun setRideBar(entity: PokemonEntity, driver: Player): Float {
return getActiveController(entity).setRideBar(entity, driver)
}
override fun canJump(entity: PokemonEntity, driver: Player): Boolean {
return getActiveController(entity).canJump(entity, driver)
}
@@ -111,19 +142,9 @@ class RunUpToFlightCompositeController : RideController {
jumpStrength: Int
): Vec3 {
val controller = getActiveController(entity)
if (controller == landController && jumpStrength >= getRuntime(entity).resolveInt(minimumJump)) {
val state = getState(entity, ::CompositeState)
val isJumping = Minecraft.getInstance().options.keyJump.isDown
//Pass the speed to the next state
val flightState = flightController.getState(entity, ::BirdAirState )
val groundState = landController.getState(entity, ::GenericLandState)
flightState.currSpeed = groundState.currSpeed
getState(entity, ::RunUpToFlightCompositeState).let {
it.activeController = flightController
entity.setBehaviourFlag(PokemonBehaviourFlag.FLYING, true)
it.timeTransitioned = entity.level().gameTime
}
}
return controller.jumpForce(entity, driver, jumpStrength)
}
@@ -139,6 +160,10 @@ class RunUpToFlightCompositeController : RideController {
override fun useAngVelSmoothing(entity: PokemonEntity): Boolean = getActiveController(entity).useAngVelSmoothing(entity)
override fun useRidingAltPose(entity: PokemonEntity, driver: Player): Boolean {
return getActiveController(entity).useRidingAltPose(entity, driver)
}
override fun rotationOnMouseXY
(entity: PokemonEntity,
driver: Player,
@@ -84,6 +84,7 @@ class RunUpToJetFlightCompositeController : RideController {
val groundState = landController.getState(entity, ::GenericLandState)
groundState.currSpeed = flightState.currSpeed
groundState.stamina = flightState.stamina
groundState.rideVel = flightState.rideVel
//Clear accumulated rotation input when transferring to ground input
flightState.currMouseXForce = 0.0
@@ -108,6 +109,7 @@ class RunUpToJetFlightCompositeController : RideController {
val groundState = landController.getState(entity, ::GenericLandState)
flightState.currSpeed = groundState.currSpeed
flightState.stamina = groundState.stamina
flightState.rideVel = groundState.rideVel
getState(entity, ::RunUpToFlightCompositeState).let {
it.activeController = flightController
@@ -14,5 +14,7 @@ import net.minecraft.world.phys.Vec3
data class BirdAirState(val entity: PokemonEntity) : RidingState {
var currSpeed: Double = 0.0
var lastVelocity: Vec3 = Vec3(0.0, 0.0, 0.0)
var stamina: Float = 1.0f
var rideVel: Vec3 = Vec3.ZERO
var gliding: Boolean = false
}
@@ -0,0 +1,87 @@
package com.cobblemon.mod.common.util
import com.cobblemon.mod.common.Cobblemon
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URI
object HttpService {
val gson: Gson = GsonBuilder().create()
inline fun <reified T> get(url: String): HttpResponse<T> {
val connection = (URI.create(url).toURL().openConnection() as HttpURLConnection).also {
it.requestMethod = "GET"
it.connectTimeout = 5000
it.readTimeout = 5000
it.useCaches = false
}
return readFromUrlConnection<T>(connection)
}
inline fun <reified T> post(url: String, body: Any? = null): HttpResponse<T> {
val bodyJson = gson.toJson(body)
val bodyBytes = bodyJson.toByteArray(Charsets.UTF_8)
val connection = (URI.create(url).toURL().openConnection() as HttpURLConnection).also {
it.requestMethod = "POST"
it.doOutput = true
it.connectTimeout = 5000
it.readTimeout = 5000
it.useCaches = false
it.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
it.setRequestProperty("Content-Length", bodyBytes.size.toString())
}
connection.connect()
connection.outputStream.use {
it.write(bodyBytes)
}
return readFromUrlConnection<T>(connection)
}
inline fun <reified T> readFromUrlConnection(connection: HttpURLConnection): HttpResponse<T> {
try {
val status = connection.responseCode
if (status >= 400) {
connection.errorStream.use {
val error = readFromStream(it)
return HttpResponse(error = HttpError(status, error), body = null)
}
}
else {
connection.inputStream.use {
val bodyStr = readFromStream(it)
return when {
T::class == Unit::class -> HttpResponse(error = null, body = null)
T::class == String::class -> HttpResponse(error = null, body = bodyStr as T)
else -> HttpResponse(error = null, body = gson.fromJson(bodyStr, T::class.java))
}
}
}
}
catch (e: Exception) {
Cobblemon.LOGGER.error("Failed to read from connection url at ${connection.url}", e)
return HttpResponse(error = HttpError(500, null), body = null)
}
}
fun readFromStream(stream: InputStream): String {
ByteArrayOutputStream().use { result ->
val buffer = ByteArray(1024)
var length: Int
while (stream.read(buffer).also { length = it } != -1) {
result.write(buffer, 0, length)
}
return result.toString("UTF-8")
}
}
}
data class HttpResponse<T>(val error: HttpError?, val body: T?)
data class HttpError(val statusCode: Int, val details: String?)
@@ -124,8 +124,14 @@
},
"pokeball": {
"scale": {
"0.4583": {"pre": [0, 0, 0], "post": [0.3, 0.3, 0.3]},
"0.5417": {"pre": [0.3, 0.3, 0.3], "post": [0.3, 0.3, 0.3]},
"0.4583": {
"pre": [0, 0, 0],
"post": [0.3, 0.3, 0.3]
},
"0.5417": {
"pre": [0.3, 0.3, 0.3],
"post": [0.3, 0.3, 0.3]
},
"0.7083": [1, 1, 1]
}
},
@@ -556,105 +562,6 @@
}
}
},
"animation.trainer_generic.send_out": {
"animation_length": 3.625,
"bones": {
"torso": {
"rotation": {
"0.0": [1, 0, 0],
"0.9375": [1, 0, 0],
"1.375": [1, -7.5, 0],
"2.875": [1, -7.5, 0],
"3.375": [1, 0, 0],
"3.625": [1, 0, 0]
}
},
"head": {
"rotation": {
"0.0": [-1, 0, 0],
"0.9375": [-1, 0, 0],
"1.375": [-1, 7.5, 0],
"2.875": [-1, 7.5, 0],
"3.375": [-1, 0, 0],
"3.625": [-1, 0, 0]
}
},
"arm_ball": {
"rotation": {
"0.0": [3, 0, 3.5],
"0.5": [3, 0, 3.5],
"0.9375": {
"pre": [14.5, 2.5, -17.5],
"post": [14.5, 2.5, -17.5],
"lerp_mode": "catmullrom"
},
"1.125": {
"post": [5, 2.5, -0.5],
"lerp_mode": "catmullrom"
},
"1.375": [-91, 7.5, 0],
"2.875": [-91, 7.5, 0],
"3.1875": {
"pre": [5, 2.5, -0.5],
"post": [5, 2.5, -0.5],
"lerp_mode": "catmullrom"
},
"3.375": {
"post": [14.5, 2.5, -17.5],
"lerp_mode": "catmullrom"
},
"3.625": [3, 0, 3.5]
},
"position": {
"0.0": [0, 0.1, 0],
"3.625": [0, 0.1, 0]
}
},
"arm_left": {
"rotation": {
"0.0": [3, 0, -3.5],
"0.9375": [3, 0, -3.5],
"1.375": [13, 0, -6],
"2.875": [13, 0, -6],
"3.375": [3, 0, -3.5]
},
"position": [0, 0.1, 0]
},
"pokeball": {
"scale": {
"0.9375": {"pre": [0, 0, 0], "post": [0.3, 0.3, 0.3]},
"1.0": {"pre": [0.3, 0.3, 0.3], "post": [0.3, 0.3, 0.3]},
"1.1875": [1, 1, 1],
"3.0625": [1, 1, 1],
"3.25": [0.4, 0.4, 0.4],
"3.375": {"pre": [0.4, 0.4, 0.4], "post": [0, 0, 0]}
}
},
"leg_right": {
"rotation": {
"0.9375": [0, 8, 0.5],
"1.375": [1, 10, 0.5],
"2.875": [1, 10, 0.5],
"3.375": [0, 8, 0.5]
},
"position": {
"0.9375": [-0.2, 0, 0],
"1.375": [-0.2, 0, -0.2],
"2.875": [-0.2, 0, -0.2],
"3.375": [-0.2, 0, 0]
}
},
"leg_left": {
"rotation": {
"0.9375": [0, -8, -0.5],
"1.375": [0, -10, -0.5],
"2.875": [0, -10, -0.5],
"3.375": [0, -8, -0.5]
},
"position": [0.2, 0, 0]
}
}
},
"animation.trainer_generic.command": {
"animation_length": 2,
"bones": {
@@ -996,6 +903,276 @@
"scale": 0
}
}
},
"animation.trainer_generic.return": {
"animation_length": 3.625,
"bones": {
"torso": {
"rotation": {
"0.0": [1, 0, 0],
"0.9375": [1, 0, 0],
"1.375": [1, -7.5, 0],
"2.875": [1, -7.5, 0],
"3.375": [1, 0, 0],
"3.625": [1, 0, 0]
}
},
"head": {
"rotation": {
"0.0": [-1, 0, 0],
"0.9375": [-1, 0, 0],
"1.375": [-1, 7.5, 0],
"2.875": [-1, 7.5, 0],
"3.375": [-1, 0, 0],
"3.625": [-1, 0, 0]
}
},
"arm_ball": {
"rotation": {
"0.0": [3, 0, 3.5],
"0.5": [3, 0, 3.5],
"0.9375": {
"pre": [14.5, 2.5, -17.5],
"post": [14.5, 2.5, -17.5],
"lerp_mode": "catmullrom"
},
"1.125": {
"post": [5, 2.5, -0.5],
"lerp_mode": "catmullrom"
},
"1.375": [-91, 7.5, 0],
"2.875": [-91, 7.5, 0],
"3.1875": {
"pre": [5, 2.5, -0.5],
"post": [5, 2.5, -0.5],
"lerp_mode": "catmullrom"
},
"3.375": {
"post": [14.5, 2.5, -17.5],
"lerp_mode": "catmullrom"
},
"3.625": [3, 0, 3.5]
},
"position": {
"0.0": [0, 0.1, 0],
"3.625": [0, 0.1, 0]
}
},
"arm_left": {
"rotation": {
"0.0": [3, 0, -3.5],
"0.9375": [3, 0, -3.5],
"1.375": [13, 0, -6],
"2.875": [13, 0, -6],
"3.375": [3, 0, -3.5]
},
"position": [0, 0.1, 0]
},
"pokeball": {
"scale": {
"0.9375": {
"pre": [0, 0, 0],
"post": [0.3, 0.3, 0.3]
},
"1.0": [0.3, 0.3, 0.3],
"1.1875": [1, 1, 1],
"3.0625": [1, 1, 1],
"3.25": [0.4, 0.4, 0.4],
"3.375": {
"pre": [0.4, 0.4, 0.4],
"post": [0, 0, 0]
}
}
},
"leg_right": {
"rotation": {
"0.9375": [0, 8, 0.5],
"1.375": [1, 10, 0.5],
"2.875": [1, 10, 0.5],
"3.375": [0, 8, 0.5]
},
"position": {
"0.9375": [-0.2, 0, 0],
"1.375": [-0.2, 0, -0.2],
"2.875": [-0.2, 0, -0.2],
"3.375": [-0.2, 0, 0]
}
},
"leg_left": {
"rotation": {
"0.9375": [0, -8, -0.5],
"1.375": [0, -10, -0.5],
"2.875": [0, -10, -0.5],
"3.375": [0, -8, -0.5]
},
"position": [0.2, 0, 0]
}
},
"timeline": {
"1.0": "q.render_item(v.actioning_pokemon_ball, 'item_right');",
"3.3125": "q.clear_items();"
}
},
"animation.trainer_generic.send_out": {
"animation_length": 2.875,
"bones": {
"torso_crouch": {
"rotation": {
"0.9167": [0, 0, 0],
"1.4167": {
"pre": [0, 10, 0],
"post": [0, 10, 0],
"lerp_mode": "catmullrom"
},
"1.5417": {
"post": [0, -10, 0],
"lerp_mode": "catmullrom"
},
"1.7917": [0, 0, 0]
}
},
"arms": {
"rotation": {
"0.9167": [0, 0, 0],
"1.4167": {
"pre": [0, 10, 0],
"post": [0, 10, 0],
"lerp_mode": "catmullrom"
},
"1.5417": {
"post": [0, -10, 0],
"lerp_mode": "catmullrom"
},
"1.7917": [0, 0, 0]
}
},
"torso_item_right": {
"rotation": [0.5, 0, 0]
},
"arms_item_right": {
"rotation": {
"0.9167": [0, 0, 0],
"1.4167": {
"pre": [0, 10, 0],
"post": [0, 10, 0],
"lerp_mode": "catmullrom"
},
"1.5417": {
"post": [0, -10, 0],
"lerp_mode": "catmullrom"
},
"1.7917": [0, 0, 0]
}
},
"arm_item_right": {
"rotation": {
"0.9167": [1, 0, 2.5],
"1.4167": {
"pre": [-85, 10, -10],
"post": [-85, 10, -10],
"lerp_mode": "catmullrom"
},
"1.5417": {
"post": [-72.5, -30, 0],
"lerp_mode": "catmullrom"
},
"1.7917": [1, 0, 2.5]
},
"position": [0, 0.1, 0]
},
"torso_item_left": {
"rotation": [0.5, 0, 0]
},
"arms_item_left": {
"rotation": {
"0.9167": [0, 0, 0],
"1.4167": {
"pre": [0, 10, 0],
"post": [0, 10, 0],
"lerp_mode": "catmullrom"
},
"1.5417": {
"post": [0, -10, 0],
"lerp_mode": "catmullrom"
},
"1.7917": [0, 0, 0]
}
},
"arm_item_left": {
"rotation": {
"0.9167": [1, 0, -2.5],
"1.4167": {
"pre": [4, 0, 0],
"post": [4, 0, 0],
"lerp_mode": "catmullrom"
},
"1.5417": {
"post": [-8, 0, 0],
"lerp_mode": "catmullrom"
},
"1.7917": [1, 0, -2.5]
},
"position": [0, 0.1, 0]
},
"torso": {
"rotation": [0.5, 0, 0]
},
"head": {
"rotation": [-0.5, 0, 0]
},
"arm_right": {
"rotation": {
"0.9167": [1, 0, 2.5],
"1.4167": {
"pre": [-85, 10, -10],
"post": [-85, 10, -10],
"lerp_mode": "catmullrom"
},
"1.5417": {
"post": [-72.5, -30, 0],
"lerp_mode": "catmullrom"
},
"1.7917": [1, 0, 2.5]
},
"position": [0, 0.1, 0]
},
"arm_left": {
"rotation": {
"0.9167": [1, 0, -2.5],
"1.4167": {
"pre": [4, 0, 0],
"post": [4, 0, 0],
"lerp_mode": "catmullrom"
},
"1.5417": {
"post": [-8, 0, 0],
"lerp_mode": "catmullrom"
},
"1.7917": [1, 0, -2.5]
},
"position": [0, 0.1, 0]
},
"leg_right": {
"rotation": [0, 2, 0.5],
"position": [-0.05, 0, 0]
},
"leg_left": {
"rotation": [0, -2, -0.5],
"position": [0.05, 0, 0]
}
},
"timeline": {
"0.0": "q.render_item(v.actioning_pokemon_ball, 'item_right');",
"1.5833": "q.render_item('air', 'item_right');"
}
}
},
"geometry.animations": {
"texturewidth": 64,
"textureheight": 64
},
"geometry.unknown": {
"texturewidth": 16,
"textureheight": 16
}
}
@@ -81,6 +81,14 @@
{"origin": [-7, 12, -2], "size": [3, 12, 4], "inflate": 0.25, "uv": [40, 32]}
]
},
{
"name": "item_right",
"parent": "arm_right",
"pivot": [-5.5, 12, -2],
"locators": {
"item_right": [-5.5, 12, -2]
}
},
{
"name": "pokeball",
"parent": "arm_ball",
@@ -99,6 +107,14 @@
{"origin": [4, 12, -2], "size": [3, 12, 4], "inflate": 0.25, "uv": [48, 48]}
]
},
{
"name": "item_left",
"parent": "arm_left",
"pivot": [5.5, 12, -2],
"locators": {
"item_left": [5.5, 12, -2]
}
},
{
"name": "leg_right",
"parent": "body",
@@ -97,6 +97,14 @@
{"origin": [-7, 12, -2], "size": [3, 11, 4], "inflate": 0.25, "uv": [40, 31]}
]
},
{
"name": "item_right",
"parent": "arm_right",
"pivot": [-5.5, 12, -2],
"locators": {
"item_right": [-5.5, 12, -2]
}
},
{
"name": "arm_left",
"parent": "torso",
@@ -106,6 +114,14 @@
{"origin": [4, 12, -2], "size": [3, 11, 4], "inflate": 0.25, "uv": [46, 48]}
]
},
{
"name": "item_left",
"parent": "arm_left",
"pivot": [5.5, 12, -2],
"locators": {
"item_left": [5.5, 12, -2]
}
},
{
"name": "leg_right",
"parent": "body",
@@ -81,6 +81,14 @@
{"origin": [-8, 12, -2], "size": [4, 12, 4], "inflate": 0.25, "uv": [40, 32]}
]
},
{
"name": "item_right",
"parent": "arm_right",
"pivot": [-6, 12, -2],
"locators": {
"item_right": [-6, 12, -2]
}
},
{
"name": "pokeball",
"parent": "arm_ball",
@@ -99,6 +107,14 @@
{"origin": [4, 12, -2], "size": [4, 12, 4], "inflate": 0.25, "uv": [48, 48]}
]
},
{
"name": "item_left",
"parent": "arm_left",
"pivot": [6, 12, -2],
"locators": {
"item_left": [6, 12, -2]
}
},
{
"name": "leg_right",
"parent": "body",
@@ -79,29 +79,18 @@
"cubes": [
{"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [40, 16]},
{"origin": [-8, 12, -2], "size": [4, 12, 4], "inflate": 0.25, "uv": [40, 32]}
]
},
{
"name": "pokeball",
"parent": "arm_ball",
"pivot": [-6, 12, 0],
"cubes": [
{"origin": [-8, 8, -2], "size": [4, 2, 4], "pivot": [-6, 10, 0], "rotation": [90, 0, 0], "uv": [0, 122]}
],
"locators": {
"beam": [-6, 8, 0]
}
},
{
"name": "pokeball_lid",
"parent": "pokeball",
"pivot": [-6, 10, 0],
"rotation": [90, 0, 0],
"cubes": [
{"origin": [-6.5, 9.5, -2.02], "size": [1, 1, 0], "uv": [0, 123]},
{"origin": [-7, 9, -2.01], "size": [2, 2, 0], "uv": [0, 124]},
{"origin": [-8, 10, -2], "size": [4, 2, 4], "uv": [0, 116]}
]
"name": "item_right",
"parent": "arm_right",
"pivot": [-6, 12, -2],
"locators": {
"item_right": [-6, 12, -2]
}
},
{
"name": "arm_left",
@@ -112,6 +101,14 @@
{"origin": [4, 12, -2], "size": [4, 12, 4], "inflate": 0.25, "uv": [48, 48]}
]
},
{
"name": "item_left",
"parent": "arm_left",
"pivot": [6, 12, -2],
"locators": {
"item_left": [6, 12, -2]
}
},
{
"name": "leg_right",
"parent": "body",
@@ -3,11 +3,16 @@
"portraitTranslation": [0.0, 1.1, 0.0],
"profileScale": 1.0,
"profileTranslation": [0.0, -0.5, 0.0],
"properties": {
"item_display_context": "THIRD_PERSON_RIGHT_HAND",
"item_right_display_context": "THIRD_PERSON_RIGHT_HAND",
"item_left_display_context": "THIRD_PERSON_LEFT_HAND"
},
"animations": {
"win": "q.bedrock_primary('trainer_generic', 'win', q.curve('symmetrical_wide'))",
"lose": "q.bedrock_primary('trainer_generic', 'lose', q.curve('symmetrical_wide'))",
"send_out": "q.bedrock_primary('trainer_generic', 'send_out', q.curve('symmetrical'), q.exclude_labels('look'))",
"recall": "q.bedrock_primary('trainer_generic', 'send_out', q.curve('symmetrical_wide'), q.exclude_labels('look'))",
"recall": "q.bedrock_primary('trainer_generic', 'return', q.curve('symmetrical_wide'), q.exclude_labels('look'))",
"punch_left": "q.primary_animation(q.punch('head', 'body', 'arm_left', 'arm_right', false), q.curve('symmetrical_wide'), q.exclude_labels('look'))",
"punch_right": "q.primary_animation(q.punch('head', 'body', 'arm_right', 'arm_left', true), q.curve('symmetrical_wide'), q.exclude_labels('look'))"
},
@@ -14543,7 +14543,7 @@
"rotation": [0, -6, -15]
},
"wing_right_elbow": {
"rotation": [0, -6, -15],
"rotation": [0, "-6", -15],
"position": [0.45, 0.1, 0]
},
"wing_right_membrane": {
@@ -20,7 +20,7 @@
"parent": "charizard",
"pivot": [0, 0, -1],
"locators": {
"root": [0, 0, -1]
"root": [0, 0, 0]
}
},
{
@@ -28,7 +28,7 @@
"parent": "charizard",
"pivot": [0, 41, -1],
"locators": {
"top": [0, 41, -1]
"top": [0, 41, 0]
}
},
{
@@ -116,8 +116,7 @@
{
"name": "head_correction",
"parent": "head",
"pivot": [0, 43.5, -0.5],
"rotation": [-95, 0, 0]
"pivot": [0, 43.5, -0.5]
},
{
"name": "head_ai",
@@ -128,15 +127,20 @@
"name": "head_angle",
"parent": "head_ai",
"pivot": [0, 43.5, -0.5],
"rotation": [95, 0, 0],
"cubes": [
{"origin": [-3.5, 41.5, -2], "size": [7, 7, 5], "uv": [48, 0]},
{"origin": [-3.5, 41.5, -4], "size": [7, 4, 2], "uv": [95, 64]},
{"origin": [-2.5, 48, -2], "size": [5, 5, 2], "inflate": 0.02, "uv": [37, 31]},
{"origin": [-2, 51.48744, -3.03033], "size": [0, 1, 1], "inflate": 0.01, "pivot": [-2, 52.51777, -2], "rotation": [45, 0, 15], "uv": [43, 42]},
{"origin": [2, 51.48744, -3.03033], "size": [0, 1, 1], "inflate": 0.01, "pivot": [2, 52.51777, -2], "rotation": [45, 0, -15], "uv": [41, 42]}
{"origin": [-3.5, 41.5, -4], "size": [7, 4, 2], "uv": [95, 64]}
]
},
{
"name": "item_hat",
"parent": "head_angle",
"pivot": [0, 45, 3],
"rotation": [-90, 0, 0],
"locators": {
"item_hat": [0, 45, 3]
}
},
{
"name": "locator_head",
"parent": "head_angle",
@@ -146,59 +150,51 @@
"head": [0, 46.5, 3]
}
},
{
"name": "locator_mouth",
"parent": "head_angle",
"pivot": [0, 48.75, -3],
"rotation": [-90, 0, 0],
"locators": {
"mouth": [0, 48.75, -3]
}
},
{
"name": "locator_special_attack",
"parent": "head_angle",
"pivot": [0, 48.75, -3],
"rotation": [-90, 0, 0],
"locators": {
"special_attack": [0, 48.75, -3]
}
},
{
"name": "locator_jaw_upper",
"parent": "head_angle",
"pivot": [0, 51.75, -2.25],
"rotation": [-90, 0, 0],
"locators": {
"jaw1": [0, 51.75, -2.25]
}
},
{
"name": "brow",
"parent": "head_angle",
"pivot": [0, 48.275, 1.765],
"cubes": [
{"origin": [-2.5, 48.55, 1], "size": [5, 0, 1], "uv": [0, 14]},
{"origin": [-3.5, 47.5, 1.53], "size": [7, 1, 1], "inflate": 0.04, "uv": [0, 12]}
]
},
{
"name": "eye",
"parent": "head_angle",
"pivot": [0, -3.5, 1.5]
},
{
"name": "locator_face",
"parent": "eye",
"parent": "head_angle",
"pivot": [0, 53.75, -0.25],
"rotation": [-90, 0, 0],
"locators": {
"face": [0, 53.75, -0.25]
}
},
{
"name": "locator_item_face",
"parent": "head_angle",
"pivot": [0, 48.5, 0.5],
"rotation": [-90, 0, 0],
"locators": {
"item_face": [0, 48.5, 0.5]
}
},
{
"name": "locator_mouth",
"parent": "head_angle",
"pivot": [0, 45.75, -2],
"rotation": [-90, 0, 0],
"locators": {
"mouth": [0, 45.75, -2],
"special": [0, 45.75, -2]
}
},
{
"name": "eyes",
"parent": "head_angle",
"pivot": [0, -3.5, 1.5]
},
{
"name": "brow",
"parent": "eyes",
"pivot": [0, 48.275, 1.765],
"cubes": [
{"origin": [-2.5, 48.55, 1], "size": [5, 0, 1], "uv": [0, 14]},
{"origin": [-3.5, 47.5, 1.53], "size": [7, 1, 1], "inflate": 0.04, "uv": [0, 12]}
]
},
{
"name": "eye_right",
"parent": "eye",
"parent": "eyes",
"pivot": [-2.75, 47.75, 0.875],
"cubes": [
{"origin": [-3.5, 47.5, 0], "size": [2, 1, 2], "inflate": 0.01, "uv": [106, 36]},
@@ -226,7 +222,7 @@
},
{
"name": "eye_left",
"parent": "eye",
"parent": "eyes",
"pivot": [2.75, 47.75, 0.875],
"cubes": [
{"origin": [1.5, 47.5, 0], "size": [2, 1, 2], "inflate": 0.01, "uv": [44, 82]},
@@ -255,6 +251,91 @@
{"origin": [2.41, 46.4, 0.5], "size": [1, 1, 1], "inflate": 0.03, "uv": [39, 86]}
]
},
{
"name": "muzzle",
"parent": "head_angle",
"pivot": [-3, 48.5, -2],
"cubes": [
{"origin": [-2.5, 48, -2], "size": [5, 5, 2], "inflate": 0.02, "uv": [37, 31]},
{"origin": [-2, 51.48744, -3.03033], "size": [0, 1, 1], "inflate": 0.01, "pivot": [-2, 52.51777, -2], "rotation": [45, 0, 15], "uv": [43, 42]},
{"origin": [2, 51.48744, -3.03033], "size": [0, 1, 1], "inflate": 0.01, "pivot": [2, 52.51777, -2], "rotation": [45, 0, -15], "uv": [41, 42]}
]
},
{
"name": "locator_jaw_upper",
"parent": "muzzle",
"pivot": [0, 51.75, -2.25],
"rotation": [-90, 0, 0],
"locators": {
"jaw1": [0, 51.75, -2.25]
}
},
{
"name": "nose",
"parent": "muzzle",
"pivot": [0, 53, 0],
"rotation": [-40, 0, 0],
"cubes": [
{"origin": [-2.5, 51.36698, -1.67861], "size": [5, 2, 2], "inflate": 0.03, "uv": [48, 12]},
{"origin": [-2, 53.40698, -1.17861], "size": [4, 0, 1], "uv": [0, 15]}
]
},
{
"name": "jaw",
"parent": "head_angle",
"pivot": [0, 45.5, -2],
"cubes": [
{"origin": [-3.5, 45.5, -4], "size": [7, 3, 2], "inflate": 0.01, "uv": [76, 20]}
]
},
{
"name": "jaw2",
"parent": "jaw",
"pivot": [0, 48.5, -4],
"rotation": [-7.5, 0, 0],
"cubes": [
{"origin": [-2, 48.49, -4], "size": [4, 4, 2], "uv": [0, 51]},
{"origin": [-1.5, 48.5, -2], "size": [3, 4, 0], "uv": [90, 0]},
{"origin": [1.5, 51.49749, -3.03033], "size": [0, 1, 1], "pivot": [1.5, 52.52782, -2], "rotation": [45, 0, -7.5], "uv": [37, 39]},
{"origin": [-1.5, 51.49749, -3.03033], "size": [0, 1, 1], "pivot": [-1.5, 52.52782, -2], "rotation": [45, 0, 7.5], "uv": [37, 38]}
]
},
{
"name": "locator_jaw_lower",
"parent": "jaw2",
"pivot": [0, 51.75, -1.75],
"locators": {
"jaw2": {
"offset": [0, 51.75, -1.75],
"rotation": [-90, 0, 0]
}
}
},
{
"name": "tongue",
"parent": "jaw",
"pivot": [0, 45.5, -2],
"cubes": [
{"origin": [-1.5, 45.5, -2], "size": [3, 3, 0], "uv": [87, 29]}
]
},
{
"name": "tongue2",
"parent": "tongue",
"pivot": [0, 48.5, -2],
"cubes": [
{"origin": [-1, 48.5, -2], "size": [2, 2, 0], "inflate": 0.01, "uv": [46, 38]}
]
},
{
"name": "tongue3",
"parent": "tongue2",
"pivot": [0, 50.5, -2],
"cubes": [
{"origin": [-1, 50.5, -2], "size": [2, 1, 0], "uv": [37, 43]},
{"origin": [-0.5, 51.5, -2], "size": [1, 1, 0], "uv": [45, 43]}
]
},
{
"name": "horns",
"parent": "head_angle",
@@ -300,72 +381,6 @@
"horn2": [2.5, 39, 2]
}
},
{
"name": "nose",
"parent": "head_angle",
"pivot": [0, 53, 0],
"rotation": [-40, 0, 0],
"cubes": [
{"origin": [-2.5, 51.36698, -1.67861], "size": [5, 2, 2], "inflate": 0.03, "uv": [48, 12]},
{"origin": [-2, 53.40698, -1.17861], "size": [4, 0, 1], "uv": [0, 15]}
]
},
{
"name": "jaw",
"parent": "head_angle",
"pivot": [0, 45.5, -2],
"cubes": [
{"origin": [-3.5, 45.5, -4], "size": [7, 3, 2], "inflate": 0.01, "uv": [76, 20]}
]
},
{
"name": "jaw2",
"parent": "jaw",
"pivot": [0, 48.5, -4],
"rotation": [-7.5, 0, 0],
"cubes": [
{"origin": [-2, 48.49, -4], "size": [4, 4, 2], "uv": [0, 51]},
{"origin": [-1.5, 48.5, -1.99], "size": [3, 4, 0], "uv": [90, 0]},
{"origin": [1.5, 51.49749, -3.03033], "size": [0, 1, 1], "pivot": [1.5, 52.52782, -2], "rotation": [45, 0, -7.5], "uv": [37, 39]},
{"origin": [-1.5, 51.49749, -3.03033], "size": [0, 1, 1], "pivot": [-1.5, 52.52782, -2], "rotation": [45, 0, 7.5], "uv": [37, 38]}
]
},
{
"name": "locator_jaw_lower",
"parent": "jaw2",
"pivot": [0, 51.75, -1.75],
"locators": {
"jaw2": {
"offset": [0, 51.75, -1.75],
"rotation": [-90, 0, 0]
}
}
},
{
"name": "tongue",
"parent": "jaw",
"pivot": [0, 45.5, -2],
"cubes": [
{"origin": [-1.5, 45.5, -2], "size": [3, 3, 0], "uv": [87, 29]}
]
},
{
"name": "tongue2",
"parent": "tongue",
"pivot": [0, 48.5, -2],
"cubes": [
{"origin": [-1, 48.5, -2], "size": [2, 2, 0], "inflate": 0.01, "uv": [46, 38]}
]
},
{
"name": "tongue3",
"parent": "tongue2",
"pivot": [0, 50.5, -2],
"cubes": [
{"origin": [-1, 50.5, -2], "size": [2, 1, 0], "uv": [37, 43]},
{"origin": [-0.5, 51.5, -2], "size": [1, 1, 0], "uv": [45, 43]}
]
},
{
"name": "arms",
"parent": "torso2",
@@ -403,6 +418,14 @@
{"origin": [-21.5, 22, -1.5], "size": [3, 2, 3], "inflate": 0.1, "uv": [0, 58]}
]
},
{
"name": "locator_item",
"parent": "hand_right",
"pivot": [-21.6, 23, 0],
"locators": {
"item": [-21.6, 23, 0]
}
},
{
"name": "locator_hand_primary",
"parent": "hand_right",
@@ -913,6 +936,14 @@
{"origin": [0, 9.5, 58.5], "size": [0, 13, 7], "pivot": [0, 14.45, 62], "rotation": [0, 90, 0], "uv": [0, 92]}
]
},
{
"name": "locator_fire",
"parent": "fire2",
"pivot": [0, 11, 62],
"locators": {
"fire": [0, 11, 62]
}
},
{
"name": "legs",
"parent": "body",
@@ -945,9 +976,10 @@
{
"name": "locator_foot_primary",
"parent": "foot_right",
"pivot": [-9, 1.5, -7],
"pivot": [-9, 1.5, -5],
"rotation": [90, 0, 0],
"locators": {
"foot_primary": [-9, 1.5, -7]
"foot_primary": [-9, 1.5, -5]
}
},
{
@@ -1009,9 +1041,10 @@
{
"name": "locator_foot_secondary",
"parent": "foot_left",
"pivot": [9, 1.5, -7],
"pivot": [9, 1.5, -5],
"rotation": [90, 0, 0],
"locators": {
"foot_secondary": [9, 1.5, -7]
"foot_secondary": [9, 1.5, -5]
}
},
{
@@ -86,12 +86,22 @@
"fly": {
"poseTypes": ["FLY"],
"isBattle": false,
"condition": "!q.is_gliding",
"animations": ["q.look('head_ai')", "q.bedrock('charizard', 'air_fly')"],
"quirks": [
"q.bedrock_quirk('charizard', 'blink')",
"q.bedrock_quirk('charizard', 'air_fly_quirk')"
]
},
"glide": {
"poseTypes": ["FLY"],
"isBattle": false,
"condition": "q.is_gliding",
"animations": ["q.look('head_ai')", "q.bedrock('charizard', 'air_glide_TEST')"],
"quirks": [
"q.bedrock_quirk('charizard', 'blink')"
]
},
"surface_idle": {
"poseTypes": ["STAND", "FLOAT"],
"isTouchingWater": true,
@@ -1253,6 +1253,7 @@
"key.cobblemon.translateportraitleft": "Translate Portrait Left",
"key.cobblemon.translateportraitright": "Translate Portrait Right",
"key.cobblemon.printmodelsettings": "Print Model Settings",
"key.cobblemon.toggleshowdowndebugui": "Toggle Showdown Debugger UI",
"key.cobblemon.categories.cobblemon.debug": "Cobblemon Debug",
"cobblemon.subtitle.poke_ball.capture_succeeded": "Poké Ball capture succeeds",
@@ -7211,4 +7212,4 @@
"cobblemon.ability.zenmode.desc": "Changes the Pokémon's shape when HP is half or less.",
"cobblemon.ability.zerotohero": "Zero to Hero",
"cobblemon.ability.zerotohero.desc": "The Pokémon transforms into its Hero Form when it switches out."
}
}
@@ -1,7 +1,7 @@
{
"hitbox": "player",
"presets": [
"cobblemon:looks_in_one_direction"
],
"variation": {
"dirt": [
@@ -97,12 +97,12 @@
"party": {
"type": "simple",
"pokemon": [
"spiritomb level=100 held_item=cobblemon:sitrus_berry ability=pressure nature=jolly moves=darkpulse,shadowball,willowisp,suckerpunch hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=252 special_attack_ev=252 speed_ev=6",
"porygonz level=100 held_item=cobblemon:life_orb ability=adaptability nature=timid moves=hyperbeam,shadowball,icebeam,thunderbolt hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=6 special_attack_ev=252 speed_ev=252",
"togekiss level=100 held_item=cobblemon:leftovers ability=serenegrace nature=timid moves=airslash,dazzlinggleam,aurasphere,thunderwave hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=6 special_attack_ev=252 speed_ev=252",
"lucario level=100 held_item=cobblemon:focus_sash ability=innerfocus nature=jolly moves=closecombat,meteormash,earthquake,extremespeed hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=6 attack_ev=252 speed_ev=252",
"milotic level=100 held_item=cobblemon:flame_orb ability=marvelscale nature=bold moves=recover,mirrorcoat,icebeam,scald hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=252 defence_ev=252 special_attack_ev=6",
"garchomp level=100 held_item=cobblemon:yache_berry ability=roughskin nature=jolly moves=dragonclaw,earthquake,swordsdance,poisonjab hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=6 attack_ev=252 speed_ev=252"
"spiritomb level=100 pokeball=master_ball held_item=cobblemon:sitrus_berry ability=pressure nature=jolly moves=darkpulse,shadowball,willowisp,suckerpunch hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=252 special_attack_ev=252 speed_ev=6",
"porygonz level=100 pokeball=great_ball held_item=cobblemon:life_orb ability=adaptability nature=timid moves=hyperbeam,shadowball,icebeam,thunderbolt hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=6 special_attack_ev=252 speed_ev=252",
"togekiss level=100 pokeball=ultra_ball held_item=cobblemon:leftovers ability=serenegrace nature=timid moves=airslash,dazzlinggleam,aurasphere,thunderwave hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=6 special_attack_ev=252 speed_ev=252",
"lucario level=100 pokeball=dive_ball held_item=cobblemon:focus_sash ability=innerfocus nature=jolly moves=closecombat,meteormash,earthquake,extremespeed hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=6 attack_ev=252 speed_ev=252",
"milotic level=100 pokeball=love_ball held_item=cobblemon:flame_orb ability=marvelscale nature=bold moves=recover,mirrorcoat,icebeam,scald hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=252 defence_ev=252 special_attack_ev=6",
"garchomp level=100 pokeball=quick_ball held_item=cobblemon:yache_berry ability=roughskin nature=jolly moves=dragonclaw,earthquake,swordsdance,poisonjab hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=6 attack_ev=252 speed_ev=252"
]
}
}
}
@@ -16,9 +16,9 @@ val bundle: Configuration by configurations.creating {
loom {
val clientConfig = runConfigs.getByName("client")
clientConfig.runDir = "runClient"
clientConfig.programArg("--username=AshKetchum")
// clientConfig.programArg("--username=GaryOak")
//This is AshKetchum's UUID so you get an Ash Ketchum skin
clientConfig.programArg("--uuid=93e4e551-589a-41cb-ab2d-435266c8e035")
// clientConfig.programArg("--uuid=93e4e551-589a-41cb-ab2d-435266c8e035")
val serverConfig = runConfigs.getByName("server")
serverConfig.runDir = "runServer"
}
@@ -59,4 +59,4 @@ tasks {
dependsOn(copyJar)
}
}
}