Compare commits

...
Author SHA1 Message Date
saultsc 508d11f3a4 battle switch with animation 2026-05-04 23:57:48 -04:00
saultsc 8590999785 preview buffs correctly showing 2026-05-04 23:34:50 -04:00
saultsc 68569b790b PreviewWidget boost information 2026-05-04 23:15:28 -04:00
saultsc b2302c8c80 more packts, im lost 2026-05-04 18:32:35 -04:00
saultsc 0594525f59 add timer, preview, and turn packets 2026-05-04 17:38:07 -04:00
saultsc 54802450df lang en_us battle ui info 2026-05-04 16:58:38 -04:00
saultsc c90bbac694 landon new battle ui frist commit 2026-05-04 16:34:17 -04:00
175 changed files with 6193 additions and 344 deletions
@@ -159,6 +159,9 @@ import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamRequestPack
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.battle.SpectateNearestBattlePacket
import com.cobblemon.mod.common.net.messages.server.battle.TeamPreviewSelectPokemonPacket
import com.cobblemon.mod.common.net.messages.server.battle.TeamPreviewSelectPokemonPacket.Companion.decode
import com.cobblemon.mod.common.net.messages.server.behaviour.DamageOnCollisionPacket
import com.cobblemon.mod.common.net.messages.server.behaviour.SetEntityBehaviourPacket
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
@@ -214,7 +217,9 @@ 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.SpectateNearestBattleHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamLeaveHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamPreviewSelectPokemonHandler
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.behaviour.DamageOnCollisionPacketHandler
@@ -402,6 +407,12 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(BattleChallengeExpiredPacket.ID, BattleChallengeExpiredPacket::decode, BattleChallengeExpiredHandler))
list.add(PacketRegisterInfo(BattleReplacePokemonPacket.ID, BattleReplacePokemonPacket::decode, BattleReplacePokemonHandler))
list.add(PacketRegisterInfo(BattleTransformPokemonPacket.ID, BattleTransformPokemonPacket::decode, BattleTransformPokemonHandler))
list.add(PacketRegisterInfo(BattleActorInformationPacket.ID, BattleActorInformationPacket::decode, BattleActorInformationHandler))
list.add(PacketRegisterInfo(BattleActorTeamPacket.ID, BattleActorTeamPacket::decode, BattleActorTeamHandler))
list.add(PacketRegisterInfo(BattleInformationPacket.ID, BattleInformationPacket::decode, BattleInformationHandler))
list.add(PacketRegisterInfo(BattleTimerPacket.ID, BattleTimerPacket::decode, BattleTimerHandler))
list.add(PacketRegisterInfo(TeamPreviewPacket.ID, TeamPreviewPacket::decode, TeamPreviewHandler))
list.add(PacketRegisterInfo(CloseTeamPreviewPacket.ID, CloseTeamPreviewPacket::decode, CloseTeamPreviewHandler))
// MultiBattleTeam Packets
list.add(PacketRegisterInfo(TeamRequestNotificationPacket.ID, TeamRequestNotificationPacket::decode, TeamRequestNotificationHandler))
@@ -570,6 +581,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(BattleSelectActionsPacket.ID, BattleSelectActionsPacket::decode, BattleSelectActionsHandler))
list.add(PacketRegisterInfo(SpectateBattlePacket.ID, SpectateBattlePacket::decode, SpectateBattleHandler))
list.add(PacketRegisterInfo(RemoveSpectatorPacket.ID, RemoveSpectatorPacket::decode, RemoveSpectatorHandler))
list.add(PacketRegisterInfo(TeamPreviewSelectPokemonPacket.ID, TeamPreviewSelectPokemonPacket::decode, TeamPreviewSelectPokemonHandler))
// Trade
list.add(PacketRegisterInfo(AcceptTradeRequestPacket.ID, AcceptTradeRequestPacket::decode, AcceptTradeRequestHandler))
@@ -644,6 +656,9 @@ object CobblemonNetwork {
// Debug / cheats
list.add(PacketRegisterInfo(RequestOpenRidingStatsDebugGUIPacket.ID, RequestOpenRidingStatsDebugGUIPacket::decode, RequestOpenRidingStatsDebugGUIHandler))
list.add(PacketRegisterInfo(SpectateNearestBattlePacket.ID, SpectateNearestBattlePacket
::decode, SpectateNearestBattleHandler))
return list
}
@@ -15,6 +15,8 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.CobblemonBuildDetails
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
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
@@ -46,14 +48,22 @@ import com.cobblemon.mod.common.battles.dispatch.WaitDispatch
import com.cobblemon.mod.common.battles.interpreter.ContextManager
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.battles.timers.PlayerBattleTimer
import com.cobblemon.mod.common.battles.timers.ShowdownTimer
import com.cobblemon.mod.common.entity.PlatformType
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorTeamPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleInitializePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleInformationDTO
import com.cobblemon.mod.common.net.messages.client.battle.BattleInformationPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleTimerPacket
import com.cobblemon.mod.common.net.messages.client.battle.FieldEffect
import com.cobblemon.mod.common.pokemon.evolution.progress.DefeatEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
import com.cobblemon.mod.common.pokemon.requirements.DefeatRequirement
@@ -69,6 +79,7 @@ import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.item.ItemStack
import java.time.Instant
/**
* Individual battle instance
@@ -80,7 +91,8 @@ import net.minecraft.world.item.ItemStack
open class PokemonBattle(
val format: BattleFormat,
val side1: BattleSide,
val side2: BattleSide
val side2: BattleSide,
val playerTimer: (PokemonBattle, PlayerBattleActor) -> PlayerBattleTimer? = { battle, actor -> ShowdownTimer(battle, actor) }
) {
/** Whether logging will be silenced for this battle. */
var mute = !CobblemonBuildDetails.SNAPSHOT
@@ -95,6 +107,9 @@ open class PokemonBattle(
side1.battle = this
side2.battle = this
this.actors.forEach { actor ->
if (actor is PlayerBattleActor) {
actor.timer = playerTimer(this, actor)
}
actor.battle = this
actor.pokemonList.forEach { battlePokemon ->
battlePokemon.effectedPokemon.evolutionProxy.current().progress()
@@ -128,6 +143,8 @@ open class PokemonBattle(
var announcingRules = false
var turn: Int = 0
private set
var turnStartTime: Instant = Instant.now()
private set
private var ticks: Int = 0
set(value) { field = value.coerceAtMost(Int.MAX_VALUE) } // go outside
@@ -137,13 +154,11 @@ open class PokemonBattle(
var dispatchResult = GO
val dispatches = ConcurrentLinkedDeque<BattleDispatch>()
val afterDispatches = mutableListOf<() -> Unit>()
val captureActions = mutableListOf<BattleCaptureAction>()
val majorBattleActions = hashMapOf<UUID, BattleMessage>()
/** Monotonically increasing counter to track faint ordering within this battle. */
var faintCounter = 0
val minorBattleActions = hashMapOf<UUID, BattleMessage>()
val contextManager = ContextManager()
@@ -213,6 +228,23 @@ open class PokemonBattle(
return actor to pokemon
}
fun startSpectating(player: ServerPlayer) {
if (!Cobblemon.config.allowSpectating) return
spectators.add(player.uuid)
player.sendPacket(BattleInitializePacket(this, null))
player.sendPacket(BattleMessagePacket(chatLog))
notifyOfUpdates(listOf(player.uuid))
actors.forEach { actor ->
val team = actor.pokemonList.map { it.toBattleDTO(false) }
player.sendPacket(BattleActorTeamPacket(actor.uuid, team))
}
}
fun stopSpectating(player: ServerPlayer) {
player.sendPacket(BattleEndPacket())
spectators.remove(player.uuid)
}
/**
* Gets a [BattlePokemon] from a pnx key and uuid.
*
@@ -238,6 +270,11 @@ open class PokemonBattle(
fun turn(newTurnNumber: Int) {
actors.forEach { it.turn() }
actors.filterIsInstance<PlayerBattleActor>().forEach { actor ->
actor.timer?.mustChooseBy()?.let {
actor.uuid.getPlayer()?.sendPacket(BattleTimerPacket(it))
}
}
// TODO: If a pokemon switches in the same turn another pokemon is KO'd it will not receive exp for the KO
for (side in sides) {
val opposite = side.getOppositeSide()
@@ -247,27 +284,49 @@ open class PokemonBattle(
}
}
this.turn = newTurnNumber
this.turnStartTime = Instant.now()
notifyAllOfUpdates()
}
fun notifyAllOfUpdates() {
val uuids = this.actors.map { it.uuid } + this.spectators
notifyOfUpdates(uuids)
}
fun notifyOfUpdates(uuids: List<UUID>) {
val weather = this.contextManager.get(BattleContext.Type.WEATHER)?.firstOrNull()
val terrain = this.contextManager.get(BattleContext.Type.TERRAIN)?.firstOrNull()
val room = this.contextManager.get(BattleContext.Type.ROOM)?.firstOrNull()
val side1SidedEffects = getSidedFieldEffects(side1)
val side2SidedEffects = getSidedFieldEffects(side2)
val updatePacket = BattleInformationPacket(this.battleId, BattleInformationDTO(
turn = turn,
weather = weather?.let { FieldEffect(it.id, it.turn, it.origin?.entity?.id) },
terrain = terrain?.let { FieldEffect(it.id, it.turn, it.origin?.entity?.id) },
room = room?.let { FieldEffect(it.id, it.turn, it.origin?.entity?.id) },
side1SidedEffects = side1SidedEffects,
side2SidedEffects = side2SidedEffects,
))
uuids.mapNotNull { it.getPlayer() }.forEach {
it.sendPacket(updatePacket)
}
}
private fun getSidedFieldEffects(side: BattleSide): List<FieldEffect> {
val hazards = side.contextManager.get(BattleContext.Type.HAZARD)?.map { FieldEffect(it.id, it.turn, it.origin?.entity?.id) } ?: emptyList()
val screens = side.contextManager.get(BattleContext.Type.SCREEN)?.map { FieldEffect(it.id, it.turn, it.origin?.entity?.id) } ?: emptyList()
val tailwind = side.contextManager.get(BattleContext.Type.TAILWIND)?.firstOrNull()?.let { FieldEffect(it.id, it.turn, it.origin?.entity?.id) }
return hazards + screens + listOfNotNull(tailwind)
}
fun end() {
ended = true
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
this.actors.forEach { actor ->
val faintedPokemons = actor.pokemonList.filter { it.health <= 0 }
actor.getSide().getOppositeSide().actors.forEach { opponent ->
val opponentPokemons = if (awardToFainted) opponent.pokemonList else opponent.pokemonList.filter { it.health > 0 }
val opponentNonFaintedPokemons = opponent.pokemonList.filter { it.health > 0 }
faintedPokemons.forEach { faintedPokemon ->
for (opponentPokemon in opponentPokemons) {
// Faint-order check: if awarding XP to fainted pokemon, only award for enemies that fainted before this pokemon did
if (awardToFainted) {
val enemyFaintedAt = faintedPokemon.faintedAt ?: continue
val opponentFaintedAt = opponentPokemon.faintedAt
if (opponentFaintedAt != null && opponentFaintedAt <= enemyFaintedAt) {
continue
}
}
for (opponentPokemon in opponentNonFaintedPokemons) {
val facedFainted = opponentPokemon.facedOpponents.contains(faintedPokemon)
val pokemon = opponentPokemon.effectedPokemon
if (facedFainted) {
@@ -288,8 +347,7 @@ open class PokemonBattle(
else -> continue
}
val experience = Cobblemon.experienceCalculator.calculate(opponentPokemon, faintedPokemon, multiplier)
val enemyTeamWiped = actor.pokemonList.all { it.health <= 0 }
if (experience > 0 && (enemyTeamWiped || awardOnLoss)) {
if (experience > 0 && actor.pokemonList.all { it.health <= 0 }) {
opponent.awardExperience(opponentPokemon, experience)
}
Cobblemon.evYieldCalculator.calculate(opponentPokemon, faintedPokemon).forEach { (stat, amount) ->
@@ -303,7 +361,7 @@ open class PokemonBattle(
val player = actor.getPlayerUUIDs().first().getPlayer()
player?.level()?.itemRegistry.let { registry ->
actor.itemsUsed.mapNotNull { registry?.get(ResourceLocation.tryBySeparator(it.itemName.substringAfter('.'), '.')) }
.forEach { player?.giveOrDropItemStack(ItemStack(it))}
.forEach { player?.giveOrDropItemStack(ItemStack(it))}
}
}
@@ -324,10 +382,16 @@ open class PokemonBattle(
actors.forEach { actor ->
actor.pokemonList.forEach { battlePokemon ->
battlePokemon.postBattlePokemonOperations.forEach { it(battlePokemon) }
battlePokemon.entity?.let { entity -> battlePokemon.postBattleEntityOperations.forEach { it(entity) } }
battlePokemon.clearBattleFeatures()
battlePokemon.entity?.let { entity ->
entity.terastallize(null)
battlePokemon.postBattleEntityOperations.forEach { it(entity) }
}
if (battlePokemon.effectedPokemon.entity != null
&& battlePokemon.effectedPokemon.entity?.beamMode == 0
&& battlePokemon.effectedPokemon.entity?.platform != PlatformType.NONE) {
&& battlePokemon.effectedPokemon.entity?.beamMode == 0
&& battlePokemon.effectedPokemon.entity?.platform != PlatformType.NONE) {
battlePokemon.effectedPokemon.tryRecallWithAnimation()
}
}
@@ -399,10 +463,12 @@ open class PokemonBattle(
fun dispatch(dispatcher: () -> DispatchResult) {
dispatches.add(BattleDispatch { dispatcher() })
}
fun dispatchToFront(dispatcher: () -> DispatchResult) {
dispatches.addFirst(BattleDispatch { dispatcher() })
}
fun dispatchWaitingToFront(delaySeconds: Float = 1F, dispatcher: () -> Unit) {
@@ -453,12 +519,24 @@ open class PokemonBattle(
dispatches.addFirst(dispatcher)
}
fun doWhenClear(action: () -> Unit) {
afterDispatches.add(action)
}
fun tick() {
if (turn >= 1) {
this.actors.filterIsInstance<PlayerBattleActor>().forEach { it.timer?.tick() }
}
try {
while (dispatchResult.canProceed()) {
val dispatch = dispatches.poll() ?: break
dispatchResult = dispatch(this)
}
if (dispatches.isEmpty()) {
afterDispatches.toList().forEach { it() }
afterDispatches.clear()
}
} catch (e: Exception) {
LOGGER.error("Exception while ticking a battle. Saving battle log.", e)
val message = battleLang("crash").red()
@@ -514,7 +592,10 @@ open class PokemonBattle(
fun checkForInputDispatch() {
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
if (checkFleeAttempt()) return
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none {
LOGGER.info("${it.getName().string} (Must Choose: ${it.mustChoose}): ${it.responses}")
it.mustChoose
})
if (readyToInput && captureActions.isEmpty()) {
actors.filter { it.responses.isNotEmpty() }.forEach { it.writeShowdownResponse() }
actors.forEach { it.responses.clear() ; it.request = null }
@@ -8,10 +8,17 @@
package com.cobblemon.mod.common.api.events
import com.cobblemon.mod.common.api.events.battles.BattleChoiceMadeEvent
import com.cobblemon.mod.common.api.events.battles.BattleChoiceRequestedEvent
import com.cobblemon.mod.common.api.events.battles.BattleFaintedEvent
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
import com.cobblemon.mod.common.api.events.battles.BattleHPEvent
import com.cobblemon.mod.common.api.events.battles.BattlePickMoveEvent
import com.cobblemon.mod.common.api.events.battles.BattleStartedEvent
import com.cobblemon.mod.common.api.events.battles.BattleSwitchEvent
import com.cobblemon.mod.common.api.events.battles.BattleTerastallizeEvent
import com.cobblemon.mod.common.api.events.battles.BattleUseMoveEvent
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
import com.cobblemon.mod.common.api.events.battles.instruction.MegaEvolutionEvent
@@ -118,6 +125,21 @@ object CobblemonEvents {
@JvmField
val POKEMON_CAPTURED = EventObservable<PokemonCapturedEvent>()
@JvmField
val BATTLE_USE_MOVE = EventObservable<BattleUseMoveEvent>()
@JvmField
val BATTLE_PICK_MOVE = EventObservable<BattlePickMoveEvent>()
@JvmField
val BATTLE_SWITCH = EventObservable<BattleSwitchEvent>()
@JvmField
val BATTLE_TERA = EventObservable<BattleTerastallizeEvent>()
@JvmField
val BATTLE_HP = EventObservable<BattleHPEvent>()
@JvmField
val FOSSIL_REVIVED = EventObservable<FossilRevivedEvent>()
@@ -157,7 +179,7 @@ object CobblemonEvents {
@JvmField
val POKEMON_SENT_POST = EventObservable<PokemonSentEvent.Post>()
@JvmField
val POKEMON_RECALL_PRE = CancelableObservable<PokemonRecallEvent.Pre>()
@@ -186,7 +208,7 @@ object CobblemonEvents {
val POKEMON_HEALED = CancelableObservable<PokemonHealedEvent>()
@JvmField
/** CLIENT ONLY! */
/** CLIENT ONLY! */
val POKEMON_INTERACTION_GUI_CREATION = EventObservable<PokemonInteractionGUICreationEvent>()
@JvmField
@@ -382,6 +404,12 @@ object CobblemonEvents {
@JvmField
val SELECT_DRIVER = EventObservable<SelectDriverEvent>()
@JvmField
val BATTLE_CHOICE_REQUESTED = EventObservable<BattleChoiceRequestedEvent>()
@JvmField
val BATTLE_CHOICE_MADE = EventObservable<BattleChoiceMadeEvent>()
// Habitats
@JvmField
val HABITAT_SPAWN_ACTIVATED = CancelableObservable<HabitatSpawnActivatedEvent>()
@@ -0,0 +1,15 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import net.minecraft.server.level.ServerPlayer
data class BattleChoiceMadeEvent (
val player: ServerPlayer
)
@@ -0,0 +1,17 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import net.minecraft.server.level.ServerPlayer
data class BattleChoiceRequestedEvent (
val player: ServerPlayer,
val packet: BattleMakeChoicePacket
)
@@ -0,0 +1,17 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
data class BattleHPEvent(
val battle: PokemonBattle,
val pokemon: BattlePokemon,
val newHealth: String
)
@@ -0,0 +1,15 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import java.util.*
data class BattlePickMoveEvent(
val player: UUID
)
@@ -0,0 +1,18 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
data class BattleStartEvent (
val playerActor: PlayerBattleActor,
val wildActor: PokemonBattleActor
): Cancelable()
@@ -0,0 +1,17 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.pokemon.Pokemon
data class BattleSwitchEvent(
val battle: PokemonBattle,
val pokemon: Pokemon
)
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import net.minecraft.server.level.ServerPlayer
data class BattleTerastallizeEvent(
val battle: PokemonBattle,
val playerEntity: ServerPlayer,
val pokemon: BattlePokemon
)
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import net.minecraft.server.level.ServerPlayer
data class BattleUseMoveEvent(
val battle: PokemonBattle,
val players: List<ServerPlayer>,
val userPokemon: BattlePokemon,
val move: MoveTemplate
)
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import net.minecraft.server.level.ServerPlayer
/**
* Event fired when a [BattlePokemon] faints. Exposes the [BattlePokemon] that fainted and the [BattleContext]
* of how it fainted.
*
* @author Segfault Guy
* @since April 6th, 2023
*/
data class GimmighoulEvent (
val playerEntity: ServerPlayer
)
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import net.minecraft.server.level.ServerPlayer
data class NPCDefineBattleActorEvent(
val player: ServerPlayer,
val npcEntity: NPCEntity,
var battleActor: NPCBattleActor
)
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.server.level.ServerPlayer
import java.util.*
data class NPCEvent(
val player: ServerPlayer,
val entity: PokemonEntity,
val leadingPokemon: UUID
)
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.pokeball.PokeBall
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.server.level.ServerPlayer
data class PokemonCapturePreEvent (
val player: ServerPlayer,
val pokemon: Pokemon,
val pokeBall: PokeBall
): Cancelable()
@@ -12,6 +12,7 @@ import com.bedrockk.molang.runtime.struct.MoStruct
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.moves.animations.ActionEffectTimeline
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.moves.categories.DamageCategory
import com.cobblemon.mod.common.api.types.ElementalType
@@ -73,6 +74,7 @@ open class MoveTemplate(
.addFunction("weight") { DoubleValue(weight.toDouble()) }
}
val displayName: MutableComponent
get() = lang("move.$name")
val description: MutableComponent
@@ -22,7 +22,10 @@ import net.minecraft.util.StringRepresentable
enum class PokedexEntryProgress : StringRepresentable, Comparable<PokedexEntryProgress> {
UNREGISTERED,
SEEN,
OWNED;
OWNED,
NONE,
CAUGHT;
override fun getSerializedName() = this.name
}
@@ -0,0 +1,132 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.pokemon.feature
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.aspect.AspectProvider
import com.cobblemon.mod.common.api.properties.CustomPokemonPropertyType
import com.cobblemon.mod.common.client.gui.summary.featurerenderers.SummarySpeciesFeatureRenderer
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.readString
import com.google.gson.JsonObject
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.RegistryFriendlyByteBuf
class BattleFormFeature(formName: String, enabled: Boolean): FlagSpeciesFeature(formName, enabled) {
override fun saveToJSON(pokemonJSON: JsonObject): JsonObject {
return pokemonJSON
}
override fun saveToNBT(pokemonNBT: CompoundTag): CompoundTag {
return pokemonNBT
}
}
class BattleFlagSpeciesFeatureProvider : SynchronizedSpeciesFeatureProvider<BattleFormFeature>,
CustomPokemonPropertyType<BattleFormFeature>, AspectProvider {
override var keys: List<String>
override val needsKey get() = true
var default: String? = null
var isAspect = true
override var visible: Boolean = false
override fun invoke(buffer: RegistryFriendlyByteBuf, name: String): BattleFormFeature? {
return if (name in keys) {
BattleFormFeature(name, false).also { it.loadFromBuffer(buffer) }
} else {
null
}
}
override fun examples() = setOf("true", "false")
internal constructor() {
this.keys = emptyList()
}
constructor(keys: List<String>) {
this.keys = keys
}
constructor(keys: List<String>, default: Boolean) {
this.keys = keys
this.default = default.toString()
}
constructor(vararg keys: String) : this(keys.toList())
override fun invoke(pokemon: Pokemon): BattleFormFeature? {
return pokemon.getFeature(keys.first())
?: when (default) {
in setOf("true", "false") -> BattleFormFeature(keys.first(), default.toBoolean())
else -> null
}
}
override fun saveToBuffer(buffer: RegistryFriendlyByteBuf, toClient: Boolean) {
buffer.writeCollection(keys) { _, value -> buffer.writeUtf(value) }
buffer.writeNullable(default) { _, value -> buffer.writeUtf(value) }
buffer.writeBoolean(isAspect)
}
override fun loadFromBuffer(buffer: RegistryFriendlyByteBuf) {
keys = buffer.readList { it.readString() }
default = buffer.readNullable { it.readString() }
isAspect = buffer.readBoolean()
}
override fun invoke(nbt: CompoundTag): BattleFormFeature? {
return if (nbt.contains(keys.first())) {
BattleFormFeature(keys.first(), false).also { it.loadFromNBT(nbt) }
} else null
}
override fun invoke(json: JsonObject): BattleFormFeature? {
return if (json.has(keys.first())) {
BattleFormFeature(keys.first(), false).also { it.loadFromJSON(json) }
} else null
}
override fun get(pokemon: Pokemon) = pokemon.getFeature<BattleFormFeature>(keys.first())
override fun getRenderer(pokemon: Pokemon): SummarySpeciesFeatureRenderer<BattleFormFeature>? {
return null
}
override fun fromString(value: String?): BattleFormFeature? {
val isWeirdValue = value != null && value !in examples()
if (isWeirdValue) {
return null
}
return if (value == null) {
BattleFormFeature(keys.first(), true)
} else {
BattleFormFeature(keys.first(), value.toBoolean())
}
}
override fun provide(pokemon: Pokemon): Set<String> {
return if (isAspect && pokemon.getFeature<FlagSpeciesFeature>(keys.first())?.enabled == true) {
setOf(keys.first())
} else {
emptySet()
}
}
override fun provide(properties: PokemonProperties): Set<String> {
return if (isAspect && properties.customProperties.filterIsInstance<FlagSpeciesFeature>().find { it.name == keys.first() }?.enabled == true) {
setOf(keys.first())
} else {
emptySet()
}
}
}
@@ -8,10 +8,16 @@
package com.cobblemon.mod.common.api.pokemon.feature
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.aspect.AspectProvider
import com.cobblemon.mod.common.api.properties.CustomPokemonPropertyType
import com.cobblemon.mod.common.client.gui.summary.featurerenderers.SummarySpeciesFeatureRenderer
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.substitute
import com.cobblemon.mod.common.util.writeString
import com.google.gson.JsonObject
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.RegistryFriendlyByteBuf
/**
@@ -21,11 +27,16 @@ import net.minecraft.network.RegistryFriendlyByteBuf
* @author Hiroku
* @since November 30th, 2022
*/
class ChoiceSpeciesFeatureProvider : AbstractChoiceSpeciesFeatureProvider<List<String>>(emptyList(), emptyList()) {
open class ChoiceSpeciesFeatureProvider(
override var keys: List<String>,
var default: String? = null,
var choices: List<String> = listOf(),
var isAspect: Boolean = true,
var aspectFormat: String = "{{choice}}"
) : SynchronizedSpeciesFeatureProvider<StringSpeciesFeature>, CustomPokemonPropertyType<StringSpeciesFeature>, AspectProvider {
override var needsKey = true
override var visible = false
override fun examples() = choices
fun getAspect(feature: StringSpeciesFeature) = aspectFormat.substitute("choice", feature.value)
override fun saveToBuffer(buffer: RegistryFriendlyByteBuf, toClient: Boolean) {
buffer.writeCollection(keys) { _, value -> buffer.writeString(value) }
@@ -45,13 +56,32 @@ class ChoiceSpeciesFeatureProvider : AbstractChoiceSpeciesFeatureProvider<List<S
needsKey = buffer.readBoolean()
}
override fun fromString(value: String?): StringSpeciesFeature? {
val lower = value?.lowercase()
if (lower == null || lower !in choices) {
return null
}
override fun getRenderer(pokemon: Pokemon): SummarySpeciesFeatureRenderer<StringSpeciesFeature>? {
return null
}
return StringSpeciesFeature(keys.first(), lower)
override fun invoke(buffer: RegistryFriendlyByteBuf, name: String): StringSpeciesFeature? {
return if (name in keys) {
StringSpeciesFeature(name, "").also { it.loadFromBuffer(buffer) }
} else {
null
}
}
fun getAllAspects(): MutableList<String> {
val aspects = choices.toMutableList()
choices.forEach {
aspects[choices.indexOf(it)] = (aspectFormat.substitute("choice", it))
}
return aspects
}
override fun examples() = choices
internal constructor(): this(emptyList())
override fun get(pokemon: Pokemon): StringSpeciesFeature? {
return pokemon.features.filterIsInstance<StringSpeciesFeature>().find { it.name in keys }
}
override fun invoke(pokemon: Pokemon): StringSpeciesFeature? {
@@ -63,8 +93,7 @@ class ChoiceSpeciesFeatureProvider : AbstractChoiceSpeciesFeatureProvider<List<S
default!!
} else if (default == "random") {
// If it's mandatory, but they provided no value and no default, give it a random value.
choices.randomOrNull()
?: throw IllegalStateException("The 'choices' list is empty for species feature provider: ${keys.joinToString()}")
choices.randomOrNull() ?: throw IllegalStateException("The 'choices' list is empty for species feature provider: ${keys.joinToString()}")
} else {
return null
}
@@ -73,11 +102,45 @@ class ChoiceSpeciesFeatureProvider : AbstractChoiceSpeciesFeatureProvider<List<S
}
}
override fun getAllAspects(): MutableList<String> {
val aspects = choices.toMutableList()
choices.forEach {
aspects[choices.indexOf(it)] = (aspectFormat.substitute("choice", it))
}
return aspects
override fun invoke(nbt: CompoundTag): StringSpeciesFeature? {
val key = keys.find { nbt.contains(it) }
if (key == null) return null
return StringSpeciesFeature(key, "").also { it.loadFromNBT(nbt) }
}
}
override fun invoke(json: JsonObject): StringSpeciesFeature? {
val key = keys.find { json.has(it) }
if (key == null) return null
return StringSpeciesFeature(key, "").also { it.loadFromJSON(json) }
}
override fun fromString(value: String?): StringSpeciesFeature? {
val lower = value?.lowercase()
if (lower == null || lower !in choices) {
return null
}
return StringSpeciesFeature(keys.first(), lower)
}
override fun provide(pokemon: Pokemon): Set<String> {
return if (isAspect) {
get(pokemon)?.let { setOf(getAspect(it)) } ?: emptySet()
} else {
emptySet()
}
}
override fun provide(properties: PokemonProperties): Set<String> {
return if (isAspect) {
val feature = properties.customProperties.filterIsInstance<StringSpeciesFeature>().find { it.name in keys }
if (feature != null) {
setOf(getAspect(feature))
} else {
emptySet()
}
} else {
emptySet()
}
}
}
@@ -70,15 +70,28 @@ class ActiveBattlePokemon(val actor: BattleActor, var battlePokemon: BattlePokem
}
val minDistance = 4.0 + widthSum
val actorDistance = actorOffset.length()
var actorDistance = actorOffset.length()
if (actorDistance < minDistance) {
val temp = actorOffset.scale(minDistance / actorDistance) ?: actorOffset
result = actorEntityPos?.subtract(temp.subtract(actorOffset))
actorOffset = temp
if (actorDistance > 0.0) {
val temp = actorOffset.scale(minDistance / actorDistance)
result = actorEntityPos?.subtract(temp.subtract(actorOffset))
actorOffset = temp
} else {
actorOffset = Vec3(minDistance, 0.0, 0.0)
result = actorEntityPos?.add(actorOffset)
}
}
// orthogonalVector is the sideways displacement from the battle actors
var orthogonalVector = Vec3(actorOffset.x, 0.0, actorOffset.z).normalize()
var orthogonalVector = Vec3(actorOffset.x, 0.0, actorOffset.z)
orthogonalVector =
if (orthogonalVector.lengthSqr() > 0.0)
orthogonalVector.normalize()
else
Vec3(0.0, 0.0, 1.0)
orthogonalVector = orthogonalVector.cross(Vec3(0.0, 1.0, 0.0))
if (battle.format.battleType.pokemonPerSide == 1) { // Singles
@@ -121,13 +134,13 @@ class ActiveBattlePokemon(val actor: BattleActor, var battlePokemon: BattlePokem
// If the collision is too close to the actor, try to find a more reasonable position.
// The motivation here is to give setPositionSafely something more reasonable to work with.
val collisionResult = entity.level().clip(
ClipContext(
actorEntityPos!!.add(Vec3(0.0,entity.eyeHeight.toDouble(),0.0)), // start position
Vec3(result.x, entity.y + entity.eyeHeight.toDouble(), result.z), // end position
ClipContext.Block.OUTLINE,
ClipContext.Fluid.NONE,
entity
)
ClipContext(
actorEntityPos!!.add(Vec3(0.0,entity.eyeHeight.toDouble(),0.0)), // start position
Vec3(result.x, entity.y + entity.eyeHeight.toDouble(), result.z), // end position
ClipContext.Block.OUTLINE,
ClipContext.Fluid.NONE,
entity
)
)
if (collisionResult.type == HitResult.Type.BLOCK) {
// Collided with terrain
@@ -28,6 +28,8 @@ data class BattleFormat(
var ruleSet: Set<String> = setOf(),
val gen: Int = 9,
var adjustLevel: Int = -1, // Stop gap rule before a more general system for rules enforced by Cobblemon is implemented.
val isOpenTeamSheet: Boolean = false,
val allowedGimmicks: Set<ShowdownMoveset.Gimmick> = ShowdownMoveset.Gimmick.entries.toSet()
) {
companion object {
fun setBattleRules(
@@ -15,8 +15,11 @@ import com.cobblemon.mod.common.api.moves.HiddenPowerUtil
import com.cobblemon.mod.common.api.pokemon.helditem.HeldItemProvider
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.battles.timers.PlayerBattleTimer
import com.cobblemon.mod.common.battles.timers.ShowdownTimer
import com.google.gson.GsonBuilder
import net.minecraft.server.level.ServerPlayer
import java.util.*
@@ -39,6 +42,10 @@ object BattleRegistry {
getBattleByParticipatingPlayer(player)?.stop()
}
fun getPlayersCurrentlyBattling(): List<ServerPlayer> {
return battleMap.values.flatMap { it.players }
}
/**
* Packs a team into the showdown format
*
@@ -198,9 +205,10 @@ object BattleRegistry {
battleFormat: BattleFormat,
side1: BattleSide,
side2: BattleSide,
canPreempt: Boolean = true
canPreempt: Boolean = true,
playerTimer: (PokemonBattle, PlayerBattleActor) -> PlayerBattleTimer? = { _, _ -> null }
): BattleStartResult {
val battle = PokemonBattle(battleFormat, side1, side2)
val battle = PokemonBattle(battleFormat, side1, side2, playerTimer)
val start: () -> Unit = {
battleMap[battle.battleId] = battle
startShowdown(battle)
@@ -217,6 +225,19 @@ object BattleRegistry {
return ErroredBattleStart(mutableSetOf(BattleStartError.canceledByEvent(preBattleEvent.reason)))
}
// Wrapper for compatibility with radgyms in java
@JvmStatic
fun startBattle(
battleFormat: BattleFormat,
side1: BattleSide,
side2: BattleSide,
canPreempt: Boolean
): BattleStartResult {
// call the known kotlin method with defaults parameter
return startBattle(battleFormat, side1, side2, canPreempt) { _, _ -> null }
}
@JvmStatic
fun closeBattle(battle: PokemonBattle) {
battle.onEndHandlers.forEach { it(battle) }
@@ -239,7 +260,11 @@ object BattleRegistry {
return battleMap.values.find { playerId in it.playerUUIDs }
}
fun getBattleBySpectatorId(playerId: UUID): PokemonBattle? {
return battleMap.values.find { playerId in it.spectators }
}
fun tick() {
battleMap.forEachValue(Long.MAX_VALUE) { it.tick() }
}
}
}
@@ -45,6 +45,10 @@ class InBattleMove {
buffer.writeEnumConstant(target)
buffer.writeBoolean(disabled)
}
override fun toString(): String {
return "InBattleMove(id='$id', move='$move', pp=$pp, maxpp=$maxpp, target=$target, disabled=$disabled, gimmickMove=$gimmickMove)"
}
}
// Defined in sim/battle-actions.ts canZMove and getMaxMove
@@ -68,4 +72,8 @@ class InBattleGimmickMove {
buffer.writeEnumConstant(target)
buffer.writeBoolean(disabled)
}
override fun toString(): String {
return "InBattleGimmickMove(move='$move', target=$target, disabled=$disabled)"
}
}
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
@@ -80,10 +81,18 @@ class ShowdownActionRequest(
ShowdownMoveset.Gimmick.TERASTALLIZATION -> cobblemonResource("tera_orb")
else -> cobblemonResource("z_ring")
}
if (!player.hasKeyItem(triggerItem)) moveset.blockGimmick(gimmick)
if (!battle.format.allowedGimmicks.contains(gimmick) || !player.hasKeyItem(triggerItem)) moveset.blockGimmick(gimmick)
}
if (moveset.canMegaEvo && moveset.moves.any { it.id == "dragonascent" } && !Cobblemon.permissionValidator.hasPermission(player, "quests.mega_rayquaza.completed", 4)) {
moveset.canMegaEvo = false
}
}
}
override fun toString(): String {
return "ShowdownActionRequest(wait=$wait, active=$active, forceSwitch=$forceSwitch, noCancel=$noCancel, side=$side)"
}
}
enum class ShowdownActionResponseType(val loader: (RegistryFriendlyByteBuf) -> ShowdownActionResponse) {
@@ -95,7 +104,8 @@ enum class ShowdownActionResponseType(val loader: (RegistryFriendlyByteBuf) -> S
SHIFT({ ShiftActionResponse()}),
HEAL_ITEM({ HealItemActionResponse("potion") }),
FORFEIT({ ForfeitActionResponse() }),
FLEE_ATTEMPT({ FleeAttemptActionResponse() }),
FLEE_ATTEMPT({ FleeAttemptActionResponse() });
}
abstract class ShowdownActionResponse(val type: ShowdownActionResponseType) {
@@ -117,6 +127,9 @@ abstract class ShowdownActionResponse(val type: ShowdownActionResponseType) {
data class MoveActionResponse(var moveName: String, var targetPnx: String? = null, var gimmickID: String? = null): ShowdownActionResponse(ShowdownActionResponseType.MOVE) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean): Boolean {
if (moveName == "struggle") {
return true
}
if (forceSwitch || showdownMoveSet == null) {
return false
}
@@ -168,6 +181,10 @@ data class MoveActionResponse(var moveName: String, var targetPnx: String? = nul
gimmickID = buffer.readNullable { buffer.readString() }
return this
}
override fun toString(): String {
return "MoveActionResponse(moveName='$moveName', targetPnx=$targetPnx, gimmickID=$gimmickID)"
}
}
data class HealItemActionResponse(var item: String) : ShowdownActionResponse(ShowdownActionResponseType.FORCE_PASS) {
@@ -189,6 +206,10 @@ data class HealItemActionResponse(var item: String) : ShowdownActionResponse(Sho
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?): String {
return "healitem ${activeBattlePokemon.getPNX()} $item"
}
override fun toString(): String {
return "HealItemActionResponse(item='$item')"
}
}
class ShiftActionResponse() : ShowdownActionResponse(ShowdownActionResponseType.SHIFT) {
@@ -200,6 +221,10 @@ class ShiftActionResponse() : ShowdownActionResponse(ShowdownActionResponseType.
return "shift"
}
override fun toString(): String {
return "ShiftActionResponse()"
}
}
data class SwitchActionResponse(var newPokemonId: UUID) : ShowdownActionResponse(ShowdownActionResponseType.SWITCH) {
override fun saveToBuffer(buffer: RegistryFriendlyByteBuf) {
@@ -227,16 +252,28 @@ data class SwitchActionResponse(var newPokemonId: UUID) : ShowdownActionResponse
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?): String {
return "switch ${activeBattlePokemon.actor.pokemonList.indexOfFirst { it.uuid == newPokemonId } + 1}"
}
override fun toString(): String {
return "SwitchActionResponse(newPokemonId=$newPokemonId)"
}
}
class DefaultActionResponse: ShowdownActionResponse(ShowdownActionResponseType.DEFAULT) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean) = true
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "default"
override fun toString(): String {
return "DefaultActionResponse()"
}
}
object PassActionResponse : ShowdownActionResponse(ShowdownActionResponseType.PASS) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean) = true
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "pass"
override fun toString(): String {
return "PassActionResponse()"
}
}
/**
@@ -254,6 +291,10 @@ class ForcePassActionResponse : ShowdownActionResponse(ShowdownActionResponseTyp
}
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "pass"
override fun toString(): String {
return "ForcePassActionResponse()"
}
}
class BagItemActionResponse(val bagItem: BagItem, val target: BattlePokemon, val data: String? = null): ShowdownActionResponse(ShowdownActionResponseType.FORCE_PASS) {
@@ -274,11 +315,19 @@ class BagItemActionResponse(val bagItem: BagItem, val target: BattlePokemon, val
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?): String {
return "useitem ${target.uuid} ${bagItem.itemName} ${bagItem.getShowdownInput(target.actor, target, data)}"
}
override fun toString(): String {
return "BagItemActionResponse(bagItem=$bagItem, target=$target, data=$data)"
}
}
class ForfeitActionResponse : ShowdownActionResponse(ShowdownActionResponseType.FORFEIT) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean) = true
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "forfeit"
override fun toString(): String {
return "ForfeitActionResponse()"
}
}
class FleeAttemptActionResponse : ShowdownActionResponse(ShowdownActionResponseType.FLEE_ATTEMPT) {
@@ -286,6 +335,7 @@ class FleeAttemptActionResponse : ShowdownActionResponse(ShowdownActionResponseT
override fun toShowdownString(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?) = "flee-attempt"
}
class ShowdownMoveset {
lateinit var moves: List<InBattleMove>
var trapped = false
@@ -379,6 +429,10 @@ class ShowdownMoveset {
else -> this.canTerastallize = null
}
}
override fun toString(): String {
return "ShowdownMoveset(moves=$moves, trapped=$trapped, canMegaEvo=$canMegaEvo, canUltraBurst=$canUltraBurst, canZMove=$canZMove, canDynamax=$canDynamax, maxMoves=$maxMoves, canTerastallize=$canTerastallize)"
}
}
class ShowdownSide {
lateinit var name: UUID
@@ -400,6 +454,10 @@ class ShowdownSide {
this.pokemon = pokemon
return this
}
override fun toString(): String {
return "ShowdownSide(name=$name, id='$id', pokemon=$pokemon)"
}
}
class ShowdownPokemon {
lateinit var ident: String
@@ -456,6 +514,10 @@ class ShowdownPokemon {
return this
}
override fun toString(): String {
return "ShowdownPokemon(ident='$ident', details='$details', condition='$condition', active=$active, moves=$moves, baseAbility='$baseAbility', pokeball='$pokeball', ability='$ability', baseTypes=$baseTypes, types=$types, commanding=$commanding, reviving=$reviving)"
}
}
/** Unwraps useless maxMoves object and initializes [ShowdownMoveset.gimmickMapping] */
object ShowdownMovesetAdapter : JsonDeserializer<ShowdownMoveset> {
@@ -9,14 +9,10 @@
package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.battles.interpreter.BasicContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.interpreter.Effect
import com.cobblemon.mod.common.api.battles.interpreter.InvalidInstructionException
import com.cobblemon.mod.common.api.battles.interpreter.MissingContext
import com.cobblemon.mod.common.api.battles.interpreter.*
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
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.text.red
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
@@ -26,7 +22,21 @@ import com.cobblemon.mod.common.battles.interpreter.instructions.*
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.runOnServer
import net.minecraft.world.level.ClipContext
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import java.util.UUID
import kotlin.collections.Iterator
import kotlin.collections.filter
import kotlin.collections.forEach
import kotlin.collections.listOf
import kotlin.collections.map
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.set
import kotlin.collections.toMutableList
import kotlin.collections.toTypedArray
import kotlin.time.measureTime
@Suppress("KotlinPlaceholderCountMatchesArgumentCount", "UNUSED_PARAMETER")
object ShowdownInterpreter {
@@ -56,15 +66,14 @@ object ShowdownInterpreter {
updateInstructionParser["-boost"] = { battle, _, message, _ -> BoostInstruction(battle, message, true) }
updateInstructionParser["-block"] = { _, _, message, _ -> BlockInstruction(message) }
updateInstructionParser["cant"] = { _, _, message, _ -> CantInstruction(message) }
updateInstructionParser["-center"] = { _, _, message, _ -> CenterInstruction(message) }
updateInstructionParser["-clearallboost"] = { _, _, message, _ -> ClearAllBoostInstruction(message) }
updateInstructionParser["-clearnegativeboost"] = { _, _, message, _ -> ClearNegativeBoostInstruction(message) }
updateInstructionParser["-clearboost"] = { _, _, message, _ -> ClearBoostInstruction(message) }
updateInstructionParser["-combine"] = { _, _, message, _ -> CombineInstruction(message) }
updateInstructionParser["-copyboost"] = { _, _, message, _ -> CopyBoostInstruction(message) }
updateInstructionParser["-crit"] = { _, instructionSet, message, _ -> CritInstruction(message, instructionSet) }
updateInstructionParser["-curestatus"] = { _, _, message, _ -> CureStatusInstruction(message) }
updateInstructionParser["detailschange"] = { _, _, message, _ -> FormeChangeInstruction(message) }
updateInstructionParser["detailschange"] = { _, _, message, _ -> DetailsChangeInstruction(message) }
updateInstructionParser["-formechange"] = { _, _, message, _ -> FormeChangeInstruction(message)}
updateInstructionParser["-endability"] = { _, _, message, _ -> EndAbilityInstruction(message) }
updateInstructionParser["-end"] = { _, _, message, _ -> EndInstruction(message) }
updateInstructionParser["-enditem"] = { _, _, message, _ -> EndItemInstruction(message) }
@@ -96,7 +105,6 @@ object ShowdownInterpreter {
updateInstructionParser["-start"] = { _, _, message, _ -> StartInstruction(message) }
updateInstructionParser["-status"] = { _, _, message, _ -> StatusInstruction(message) }
updateInstructionParser["-supereffective"] = { _, instructionSet, message, _ -> SuperEffectiveInstruction(message, instructionSet) }
updateInstructionParser["swap"] = { _, instructionSet, message, _ -> SwapInstruction(message, instructionSet) }
updateInstructionParser["-swapboost"] = { _, _, message, _ -> SwapBoostInstruction(message) }
updateInstructionParser["-swapsideconditions"] = { _, _, message, _ -> SwapSideConditionsInstruction(message) }
updateInstructionParser["-terastallize"] = { _, _, message, _ -> TerastallizeInstruction(message) }
@@ -104,35 +112,37 @@ object ShowdownInterpreter {
updateInstructionParser["turn"] = { _, _, message, _ -> TurnInstruction(message) }
updateInstructionParser["-unboost"] = { battle, _, message, _ -> BoostInstruction(battle, message, false) }
updateInstructionParser["upkeep"] = { _, _, _, _ -> UpkeepInstruction() }
updateInstructionParser["-waiting"] = { _, _, message, _ -> WaitingInstruction(message) }
updateInstructionParser["-weather"] = { _, _, message, _ -> WeatherInstruction(message) }
updateInstructionParser["win"] = { _, _, message, _ -> WinInstruction(message) }
updateInstructionParser["-zbroken"] = { _, _, message, _ -> ZBrokenInstruction(message) }
updateInstructionParser["-zpower"] = { _, _, message, _ -> ZPowerInstruction(message) }
updateInstructionParser["swap"] = { _, instructionSet, message, _ -> SwapInstruction(message, instructionSet) }
updateInstructionParser["-center"] = { _, _, message, _ -> CenterInstruction(message) }
sideInstructionParser["error"] = { _, targetActor, _, message -> ErrorInstruction(targetActor, message) }
sideInstructionParser["request"] = { _, targetActor, _, message -> RequestInstruction(targetActor, message) }
splitInstructionParser["-damage"] = { _, targetActor, instructionSet, publicMessage, privateMessage, _ ->
DamageInstruction(instructionSet, targetActor, publicMessage, privateMessage)
}
DamageInstruction(instructionSet, targetActor, publicMessage, privateMessage)
}
splitInstructionParser["drag"] = { _, targetActor, instructionSet, publicMessage, privateMessage, _ ->
DragInstruction(instructionSet, targetActor, publicMessage, privateMessage)
}
DragInstruction(instructionSet, targetActor, publicMessage, privateMessage)
}
splitInstructionParser["-heal"] = { _, targetActor, _, publicMessage, privateMessage, _ ->
HealInstruction(targetActor, publicMessage, privateMessage)
}
HealInstruction(targetActor, publicMessage, privateMessage)
}
splitInstructionParser["-sethp"] = { _, targetActor, _, publicMessage, privateMessage, _ ->
SetHpInstruction(targetActor, publicMessage, privateMessage)
}
SetHpInstruction(targetActor, publicMessage, privateMessage)
}
splitInstructionParser["switch"] = { _, targetActor, instructionSet, publicMessage, privateMessage, _ ->
SwitchInstruction(instructionSet, targetActor, publicMessage, privateMessage)
}
SwitchInstruction(instructionSet, targetActor, publicMessage, privateMessage)
}
// Note '-cureteam' is a legacy thing that is only used in generation 2 and 4 mods for heal bell and aromatherapy respectively as such we can just ignore that
}
fun interpretMessage(battleId: UUID, message: String) {
// Check key map and use function if matching
if (message.startsWith("{\"winner\":\"")) {
@@ -161,6 +171,7 @@ object ShowdownInterpreter {
val instructionSet = InstructionSet()
val battleMessages = mutableListOf<BattleMessage>()
try {
val lines = rawMessage.split("\n").toMutableList()
if (lines[0] == "update") {
@@ -174,7 +185,6 @@ object ShowdownInterpreter {
val instruction = updateInstructionParser[id]?.invoke(battle, instructionSet, message, iterator) ?: UnknownInstruction(message)
instructionSet.instructions.add(instruction)
}
instructionSet.instructions.add(PostUpdateInstruction)
}
else if (lines[0] == "sideupdate") {
val showdownId = lines[1]
@@ -212,6 +222,9 @@ object ShowdownInterpreter {
// Broadcasts a generic lang to notify players of ability activations (effects are broadcasted separately)
fun broadcastAbility(battle: PokemonBattle, effect: Effect, pokemon: BattlePokemon) {
pokemon.revealedAbility = effect.typelessData
pokemon.sendUpdate()
if (effect.type != Effect.Type.ABILITY) return
battle.dispatchWaiting(0.5F) {
val lang = battleLang("ability.generic", pokemon.getName(), effect.typelessData).yellow()
battle.broadcastChatMessage(lang)
@@ -236,7 +249,7 @@ object ShowdownInterpreter {
cause.effect()?.let { effect ->
val damagingContexts = BattleContext.Type.values().filter { it.damaging }
val contextBuckets = damagingContexts.map { pokemon.contextManager.get(it) ?: side.contextManager.get(it)
?: battle.contextManager.get(it) }
?: battle.contextManager.get(it) }
ContextManager.scoop(effect.id, *contextBuckets.toTypedArray())
} ?:
// damage from moves and suicide
@@ -321,4 +334,5 @@ object ShowdownInterpreter {
fun registerSideInstructionParser(id: String, callback: (PokemonBattle, BattleActor, InstructionSet, BattleMessage) -> InterpreterInstruction) {
sideInstructionParser[id] = callback
}
}
@@ -0,0 +1,140 @@
/*
* 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
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.net.messages.client.battle.CloseTeamPreviewPacket
import com.cobblemon.mod.common.net.messages.client.battle.TeamPreviewPacket
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.party
import com.cobblemon.mod.common.util.safeParty
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import java.time.Duration
import java.time.Instant
import java.util.UUID
object TeamPreviewManager {
private val previews = mutableListOf<TeamPreview>()
val TEAM_PREVIEW_TIME_SECONDS = 60
fun tick() {
val expired = previews.filter { Duration.between(it.started, Instant.now()).seconds >= TEAM_PREVIEW_TIME_SECONDS }
expired.forEach { cancel(it) }
val ready = previews.filter {
hasEligibleSelections(it.sideA) && hasEligibleSelections(it.sideB)
}
ready.forEach { start(it) }
}
fun add(sideA: TeamPreviewSide, sideB: TeamPreviewSide, openTeamSheet: Boolean, onStart: (TeamPreview) -> Unit, onCancel: (TeamPreview) -> Unit) {
if (previews.any { it.sideA.uuid == sideA.uuid || it.sideB.uuid == sideA.uuid || it.sideA.uuid == sideB.uuid || it.sideB.uuid == sideB.uuid }) return
val now = Instant.now()
previews.add(TeamPreview(sideA, sideB, now, onStart, onCancel))
if (sideA.isPlayer) {
sideA.uuid.getPlayer()?.sendPacket(
TeamPreviewPacket(
sideA.neededSelections,
sideA.party,
sideB.party,
openTeamSheet,
now.plusSeconds(TEAM_PREVIEW_TIME_SECONDS.toLong()),
sideB.name
)
)
}
if (sideB.isPlayer) {
sideB.uuid.getPlayer()?.sendPacket(
TeamPreviewPacket(
sideB.neededSelections,
sideB.party,
sideA.party,
openTeamSheet,
now.plusSeconds(TEAM_PREVIEW_TIME_SECONDS.toLong()),
sideA.name
)
)
}
}
fun remove(player: ServerPlayer) {
val preview = previews.firstOrNull { it.sideA.isPlayer && it.sideA.uuid == player.uuid || it.sideB.isPlayer && it.sideB.uuid == player.uuid } ?: return
cancel(preview)
}
fun cancel(preview: TeamPreview) {
preview.onCancel(preview)
if (preview.sideA.isPlayer) {
preview.sideA.uuid.getPlayer()?.sendPacket(CloseTeamPreviewPacket())
}
if (preview.sideB.isPlayer) {
preview.sideB.uuid.getPlayer()?.sendPacket(CloseTeamPreviewPacket())
}
previews.remove(preview)
}
fun start(preview: TeamPreview) {
preview.onStart(preview)
if (preview.sideA.isPlayer) {
preview.sideA.uuid.getPlayer()?.sendPacket(CloseTeamPreviewPacket())
}
if (preview.sideB.isPlayer) {
preview.sideB.uuid.getPlayer()?.sendPacket(CloseTeamPreviewPacket())
}
previews.remove(preview)
}
fun changeSelections(uuid: UUID, selections: Set<Int>) {
val preview = previews.firstOrNull { it.sideA.uuid == uuid || it.sideB.uuid == uuid } ?: return
val isSideA = preview.sideA.uuid == uuid
previews.remove(preview)
if (isSideA) {
previews.add(
preview.copy(
sideA = preview.sideA.copy(selections = selections),
)
)
}
else {
previews.add(
preview.copy(
sideB = preview.sideB.copy(selections = selections),
)
)
}
}
fun hasEligibleSelections(side: TeamPreviewSide): Boolean {
if (side.selections.size < side.neededSelections) return false
if (side.selections.any { side.party.size <= it || side.party[it] == null }) return false
return true
}
}
data class TeamPreview(
val sideA: TeamPreviewSide,
val sideB: TeamPreviewSide,
val started: Instant,
val onStart: (TeamPreview) -> Unit,
val onCancel: (TeamPreview) -> Unit
)
data class TeamPreviewSide(
val uuid: UUID,
val name: Component,
val isPlayer: Boolean,
val party: List<Pokemon?>,
val selections: Set<Int>,
val neededSelections: Int
) {
constructor(player: ServerPlayer, neededSelections: Int): this(player.uuid, player.name, true, player.party().toGappyList(), emptySet(), neededSelections)
constructor(pokemon: Pokemon, name: Component? = null): this(pokemon.uuid, name ?: pokemon.getDisplayName(), false, listOf(null, null, pokemon, null, null, null, null), setOf(2), 1)
}
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pokemon.experience.BattleExperienceSource
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.timers.PlayerBattleTimer
import com.cobblemon.mod.common.net.messages.client.battle.BattleMusicPacket
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.getPlayer
@@ -47,6 +48,8 @@ class PlayerBattleActor(
field = value
}
var timer: PlayerBattleTimer? = null
override fun getName(): MutableComponent = this.entity?.name?.copy() ?: "Offline Player".red()
override fun nameOwned(name: String): MutableComponent = battleLang("owned_pokemon", this.getName(), name)
override val type = ActorType.PLAYER
@@ -31,6 +31,7 @@ import kotlin.collections.associateWith
import kotlin.collections.partition
import kotlin.math.absoluteValue
/**
* AI that tries to choose the best move for the given situations. Based off of the Pokemon Trainer Tournament Simulator Github
* https://github.com/cRz-Shadows/Pokemon_Trainer_Tournament_Simulator/blob/main/pokemon-showdown/sim/examples/Simulation-test-1.ts#L330
@@ -48,7 +49,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
private val hpWeightConsideration = 0.25 // how much HP difference is a consideration for switchins
private val hpFractionCoefficient = 0.4 // how much HP differences should be taken into account for switch ins
private val boostWeightCoefficient = 1 // the amount of boosts considered a baseline to be removed
private val switchOutMatchupThreshold = -2 // todo change this to get it feeling just right (-7 never switches)
private val switchOutMatchupThreshold = 0 // todo change this to get it feeling just right (-7 never switches)
private val selfKoMoveMatchupThreshold = 0.3
private val trickRoomThreshold = 85
private val recoveryMoveThreshold = 0.50
@@ -218,14 +219,19 @@ class StrongBattleAI(skill: Int) : BattleAI {
.filter { it.second.canBeSentOut() }
if (forceSwitch || activeBattlePokemon.isGone()) {
// When forced to switch, pick the Pokemon with the best matchup estimation
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
?: return PassActionResponse
val bestSwitch = availableSwitches.firstOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) == bestSwitchScore }
?: return PassActionResponse
if (battle.turn == 1) {
val switchTo = activeBattlePokemon.actor.pokemonList.filter { it.canBeSentOut() }.randomOrNull()
?: return DefaultActionResponse()
switchTo.willBeSwitchedIn = true
return SwitchActionResponse(switchTo.uuid)
}
else {
val bestEstimation = availableSwitches.maxByOrNull { estimateMatchup(activeBattlePokemon, aiSide, battle, it.first) }
?: return PassActionResponse
bestSwitch.second.willBeSwitchedIn = true
return SwitchActionResponse(bestSwitch.second.uuid)
bestEstimation.second.willBeSwitchedIn = true
return SwitchActionResponse(bestEstimation.second.uuid)
}
}
if (moveset == null) {
return PassActionResponse
@@ -264,6 +270,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
if (checkSwitchOutSkill() && shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
considerSwitching(activeBattlePokemon, activeTrackerPokemon, opponents, availableMoves, availableSwitches, battle, aiSide)
}
activeTrackerPokemon.firstTurn = false
// If HP is below 30% and not switching out, always use the most damaging move
if (activeTrackerPokemon.currentHpPercent < 0.3 && !shouldSwitchOut(aiSide, battle, activeBattlePokemon, moveset)) {
@@ -289,12 +296,14 @@ class StrongBattleAI(skill: Int) : BattleAI {
return chooseMove(fakeOut.first, activeBattlePokemon, targets.firstOrNull { validTarget != null && validTarget.id == it.battlePokemon!!.uuid})
}
activeTrackerPokemon.firstTurn = false
// Explosion/Self destruct
availableMoves.firstOrNull {
(it.first.id == "explosion" || it.first.id == "selfdestruct")
&& activeTrackerPokemon.currentHpPercent < selfKoMoveMatchupThreshold
&& opponents.any { opponent -> ElementalTypes.GHOST !in (opponent.form?.types ?: opponent.species!!.types) && opponent.currentHpPercent > 0.5 }
&& activeTrackerPokemon.currentHpPercent < selfKoMoveMatchupThreshold
&& opponents.any { opponent -> ElementalTypes.GHOST !in (opponent.form?.types ?: opponent.species!!.types) && opponent.currentHpPercent > 0.5 }
}?.let {
return chooseMove(it.first, activeBattlePokemon)
}
@@ -364,7 +373,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
for (move in availableMoves) {
if (move.first.id == "courtchange"
&& (!AIUtility.entryHazards.none { it in activeTracker.alliedSide.sideHazards }
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
|| setOf("tailwind", "lightscreen", "reflect").any { it in activeTracker.opponentSide.sideHazards })
&& setOf("tailwind", "lightscreen", "reflect").none { it in activeTracker.opponentSide.sideHazards }
&& AIUtility.entryHazards.none { it in activeTracker.opponentSide.sideHazards }) {
return chooseMove(move.first, activeBattlePokemon)
@@ -383,8 +392,8 @@ class StrongBattleAI(skill: Int) : BattleAI {
for (move in availableMoves) {
if (move.first.id == "bellydrum"
&& (activeTrackerPokemon.currentHpPercent > 0.6
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|| activeTrackerPokemon.currentHpPercent > 0.8)
&& activeTrackerPokemon.pokemon!!.heldItem().item == CobblemonItems.SITRUS_BERRY
|| activeTrackerPokemon.currentHpPercent > 0.8)
&& activeTrackerPokemon.boosts.getOrDefault(Stats.ATTACK, 0) < 1) {
return chooseMove(move.first, activeBattlePokemon)
}
@@ -502,7 +511,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// TODO: Need to check for Safeguard and Misty Terrain
val ability = target.pokemon?.ability?.name ?: target.currentAbility ?: return false
return listOf("comatose", "purifyingsalt").contains(ability) ||
(activeTracker.currentWeather == "sunny" && ability == "leafguard")
(activeTracker.currentWeather == "sunny" && ability == "leafguard")
}
fun shouldSwitchOut(side: BattleSide, battle: PokemonBattle, activeBattlePokemon: ActiveBattlePokemon, moveset: ShowdownMoveset): Boolean {
@@ -511,10 +520,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
return false
val activeTrackerPokemon = activeTracker.alliedSide.activePokemon.first { it.id == activeBattlePokemon.battlePokemon!!.uuid }
// Don't switch out if we just switched in
if (activeTrackerPokemon.firstTurn)
return false
val actorTracker = activeTracker.alliedSide.actors.first { activeTrackerPokemon in it.activePokemon }
val availableSwitches = actorTracker.party.filter { it.currentHp!! > 0 }
val currentAbility = activeTrackerPokemon.pokemon!!.ability
@@ -527,7 +532,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
// Only switch if a switch-in is significantly better than the current Pokémon
val currentScore = estimateMatchup(activeBattlePokemon, side, battle)
val bestSwitchScore = availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, side, battle, it) } ?: currentScore
val improvementThreshold = currentScore.absoluteValue * 0.5 + 3
val improvementThreshold = currentScore.absoluteValue * 0.25 + 2
if (bestSwitchScore <= currentScore + improvementThreshold) {
return false
}
@@ -573,13 +578,13 @@ class StrongBattleAI(skill: Int) : BattleAI {
val (physicalAttackers, specialAttackers) = opponentActiveTracker
.partition { (it.form?.baseStats ?: it.species!!.baseStats).getOrDefault(Stats.ATTACK, 0) >
(it.form?.baseStats ?: it.species!!.baseStats).getOrDefault(Stats.SPECIAL_ATTACK, 0) }
(it.form?.baseStats ?: it.species!!.baseStats).getOrDefault(Stats.SPECIAL_ATTACK, 0) }
if (physicalAttackers.any { it.boosts.getOrDefault(Stats.ATTACK, 0) <= -3 } ||
specialAttackers.any { it.boosts.getOrDefault(Stats.SPECIAL_ATTACK, 0) <= -3 } )
return true
return estimateMatchup(activeBattlePokemon, side, battle) < switchOutMatchupThreshold &&
activeTrackerPokemon.currentHpPercent > hpSwitchOutThreshold
activeTrackerPokemon.currentHpPercent > hpSwitchOutThreshold
}
fun statEstimationActive(mon: TrackerPokemon, stat: Stat): Double {
@@ -843,9 +848,9 @@ class StrongBattleAI(skill: Int) : BattleAI {
.filterNot { (calculateDamage(move.first, activeTrackerPokemon, it) >= (it.currentHpPercent * statusDamageConsiderationThreshold)) }
.filter {
(if (isPersistent) it.currentStatus == null else it.currentVolatile != status) &&
it.currentHpPercent > 0.3 &&
AIUtility.canAffectWithStatus(status, it.species!!.types, it.currentAbility) &&
!hasMajorStatusImmunity(it)
it.currentHpPercent > 0.3 &&
AIUtility.canAffectWithStatus(status, it.species!!.types, it.currentAbility) &&
!hasMajorStatusImmunity(it)
}
for (potentialTarget in sensibleTargets) {
when (status) {
@@ -857,9 +862,9 @@ class StrongBattleAI(skill: Int) : BattleAI {
}
Statuses.PARALYSIS.showdownName -> {
val electricVersusGround = move.second.getEffectiveElementalType(activeTrackerPokemon.pokemon) == ElementalTypes.ELECTRIC &&
potentialTarget.species!!.types.any { type -> type.name == "ground"}
potentialTarget.species!!.types.any { type -> type.name == "ground"}
val stats = potentialTarget.species!!.baseStats.getOrDefault(Stats.SPEED, 0) >
activeTrackerPokemon.pokemon!!.species.baseStats.getOrDefault(Stats.SPEED, 0)
activeTrackerPokemon.pokemon!!.species.baseStats.getOrDefault(Stats.SPEED, 0)
if (!electricVersusGround && stats) {
return chooseMove(move.first, activeBattlePokemon, singleTargetList.first { it.battlePokemon!!.uuid == potentialTarget.id })
}
@@ -882,14 +887,14 @@ class StrongBattleAI(skill: Int) : BattleAI {
val (allies, opponents) = multiTargetList.partition { it.isAllied(activeBattlePokemon) }
val possibleAffectedAllies = activeTracker.alliedSide.activePokemon.filter { it.id in allies.map { target -> target.battlePokemon!!.uuid } }.count {
(if (isPersistent) it.currentStatus == null else it.currentVolatile != status) &&
it.currentHpPercent > 0.3 &&
AIUtility.canAffectWithStatus(status, it.species!!.types, it.currentAbility) &&
!hasMajorStatusImmunity(it) }
it.currentHpPercent > 0.3 &&
AIUtility.canAffectWithStatus(status, it.species!!.types, it.currentAbility) &&
!hasMajorStatusImmunity(it) }
val possibleAffectedOpponents = activeTracker.opponentSide.activePokemon.filter { it.id in opponents.map { target -> target.battlePokemon!!.uuid } }.count {
(if (isPersistent) it.currentStatus == null else it.currentVolatile != status) &&
it.currentHpPercent > 0.3 &&
AIUtility.canAffectWithStatus(status, it.species!!.types, it.currentAbility) &&
!hasMajorStatusImmunity(it) }
it.currentHpPercent > 0.3 &&
AIUtility.canAffectWithStatus(status, it.species!!.types, it.currentAbility) &&
!hasMajorStatusImmunity(it) }
if (possibleAffectedAllies > possibleAffectedOpponents)
continue
@@ -990,4 +995,6 @@ class StrongBattleAI(skill: Int) : BattleAI {
return MoveActionResponse(move.id, chosenTarget.getPNX())
}
}
}
}
@@ -51,6 +51,7 @@ class AbilityInstruction(val instructionSet: InstructionSet, val message: Battle
else -> when (effect.id) {
"sturdy", "unnerve", "anticipation" -> battleLang("ability.${effect.id}", pokemonName) // Unique message
"airlock", "cloudnine" -> battleLang("ability.airlock") // Cloud Nine shares the same text as Air Lock
"chromaticscales" -> battleLang("ability.chromaticscales", pokemonName, extractLastPart(message.rawMessage))
else -> null // Effect broadcasted by a succeeding instruction
}
}
@@ -63,4 +64,15 @@ class AbilityInstruction(val instructionSet: InstructionSet, val message: Battle
else return@dispatch GO
}
}
fun extractLastPart(input: String): String {
val pattern = Regex("\\|([^|]+)\\|([^|]+)$")
val matchResult = pattern.find(input)
return if (matchResult != null) {
val (secondToLast, last) = matchResult.destructured
"$secondToLast and $last"
} else {
""
}
}
}
@@ -25,6 +25,8 @@ import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.concurrent.CompletableFuture
import kotlin.math.max
import kotlin.math.min
/**
* Format: |-boost|POKEMON|STAT|AMOUNT or |-unboost|POKEMON|STAT|AMOUNT
@@ -77,6 +79,15 @@ class BoostInstruction(battle: PokemonBattle, val message: BattleMessage, val is
val rootKey = if (isBoost) "boost" else "unboost"
battle.dispatch {
val stagesWithSign = if (isBoost) stages else -stages
if (pokemon.boosts[Stats.getStat(statKey)] == null) {
pokemon.boosts[Stats.getStat(statKey)] = stagesWithSign
}
else {
val curr = pokemon.boosts[Stats.getStat(statKey)]!!
pokemon.boosts[Stats.getStat(statKey)] = min(max(curr + stagesWithSign, -6), 6)
}
val lang = when {
message.hasOptionalArgument("zeffect") -> battleLang("$rootKey.$severity.zeffect", pokemon.getName(), stat)
else -> battleLang("$rootKey.$severity", pokemon.getName(), stat)
@@ -88,6 +99,7 @@ class BoostInstruction(battle: PokemonBattle, val message: BattleMessage, val is
// TODO: replace with context that tracks detailed information such as # of stages
repeat(stages) { pokemon.contextManager.add(context) }
battle.minorBattleActions[pokemon.uuid] = message
pokemon.sendUpdate()
return@dispatch UntilDispatch { "effects" !in holds }
}
}
@@ -27,6 +27,8 @@ class ClearAllBoostInstruction(val message: BattleMessage): InterpreterInstructi
battle.dispatchWaiting(1.5F) {
battle.activePokemon.forEach {
it.battlePokemon?.contextManager?.clear(BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
it.battlePokemon?.boosts?.clear()
it.battlePokemon?.sendUpdate()
}
battle.broadcastChatMessage(battleLang("clearallboost"))
}
@@ -32,6 +32,8 @@ class ClearBoostInstruction(val message: BattleMessage): InterpreterInstruction
battle.broadcastChatMessage(lang)
battlePokemon.contextManager.clear(BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
battle.minorBattleActions[battlePokemon.uuid] = message
battlePokemon.boosts.clear()
battlePokemon.sendUpdate()
}
}
}
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.removeIf
/**
* Format: |-clearnegativeboost|POKEMON
@@ -36,6 +37,8 @@ class ClearNegativeBoostInstruction(val message: BattleMessage): InterpreterInst
battle.broadcastChatMessage(lang)
}
battlePokemon.boosts.removeIf { it.value < 0 }
battlePokemon.sendUpdate()
battlePokemon.contextManager.clear(BattleContext.Type.UNBOOST)
battle.minorBattleActions[battlePokemon.uuid] = message
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.text.lightPurple
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattleHealthChangePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleSwitchPokemonPacket
import com.cobblemon.mod.common.util.battleLang
import kotlin.math.roundToInt
/**
* Format: |detailschange|POKEMON|DETAILS|HP STATUS
*
* POKEMON has changed formes permanently (i.e. Mega Evolution) to DETAILS.
* @author Segfault Guy
* @since September 10th, 2023
*/
class DetailsChangeInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val (pnx, _) = message.pnxAndUuid(0) ?: return
val battlePokemon = message.battlePokemon(0, battle) ?: return
val heldItemName = battlePokemon.effectedPokemon.heldItem().hoverName.string
val formName = message.argumentAt(1)?.split(',')?.get(0)?.substringAfter('-')?.lowercase() ?: return
val pokemonName = battlePokemon.getName()
val effectiveForm = when (formName) {
"mega-x" -> "mega_x"
"mega-y" -> "mega_y"
else -> formName
}
val delay = if (effectiveForm in setOf("mega_x", "mega_y", "mega")) 3.5f else 1f
battle.dispatchWaiting(delaySeconds = delay) {
battlePokemon.setBattleFeature(effectiveForm, true)
battlePokemon.sendUpdate()
val flatHp = message.argumentAt(1)?.split("/")?.getOrNull(0)?.toFloatOrNull() ?: return@dispatchWaiting
val ratioHp = message.argumentAt(1)?.split("/")?.getOrNull(0)?.toFloatOrNull()?.times(0.01F) ?: return@dispatchWaiting
battlePokemon.effectedPokemon.currentHealth = flatHp.roundToInt()
battle.sendSidedUpdate(battlePokemon.actor, BattleHealthChangePacket(pnx, flatHp), BattleHealthChangePacket(pnx, ratioHp))
battle.sendUpdate(BattleSwitchPokemonPacket(pnx, battlePokemon, true, battlePokemon.getIllusion()))
battle.broadcastChatMessage(battleLang("detailschange.$formName", pokemonName, heldItemName).lightPurple())
battle.majorBattleActions[battlePokemon.uuid] = message
}
}
}
@@ -51,9 +51,10 @@ class FaintInstruction(battle: PokemonBattle, val message: BattleMessage) : Inte
preamble.thenAccept {
faintingPokemon.effectedPokemon.currentHealth = 0
faintingPokemon.faintedAt = battle.faintCounter++
pokemon.boosts.clear()
faintingPokemon.sendUpdate()
CobblemonEvents.BATTLE_FAINTED.post(BattleFaintedEvent(battle, pokemon, context, pnx))
faintingPokemon.clearBattleFeatures()
battle.getActorAndActiveSlotFromPNX(pnx).second.battlePokemon = null
faintingPokemon.contextManager.add(context)
faintingPokemon.contextManager.clear(BattleContext.Type.STATUS, BattleContext.Type.VOLATILE, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
@@ -20,6 +20,7 @@ import com.cobblemon.mod.common.net.messages.client.battle.BattleInitializePacke
import com.cobblemon.mod.common.net.messages.client.battle.BattleMusicPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleSetTeamPokemonPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorTeamPacket
/**
* Format: |start
@@ -58,6 +59,18 @@ class InitializeInstruction(val instructionSet: InstructionSet, val message: Bat
battle.actors.forEach { actor ->
actor.sendUpdate(BattleSetTeamPokemonPacket(actor.pokemonList.map { it.effectedPokemon }))
val allyTeam = actor.pokemonList.map { it.toBattleDTO(ally = true) }
val nonallyTeam = actor.pokemonList.map { it.toBattleDTO(ally = false) }
actor.sendUpdate(BattleActorTeamPacket(actor.uuid, allyTeam))
val otherActors = battle.actors.filter { it != actor }
otherActors.forEach { other ->
val packet = BattleActorTeamPacket(actor.uuid, nonallyTeam)
other.sendUpdate(packet)
}
val req = actor.request ?: return@forEach
actor.sendUpdate(BattleQueueRequestPacket(req))
}
@@ -70,6 +83,7 @@ class InitializeInstruction(val instructionSet: InstructionSet, val message: Bat
battle.started = true
battle.side1.playCries()
afterOnServer(seconds = 1.0F) { battle.side2.playCries() }
battle.notifyAllOfUpdates()
}
}
}
@@ -12,9 +12,12 @@ import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.interpreter.Effect
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleUseMoveEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addBattleMessageFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
@@ -31,13 +34,14 @@ import com.cobblemon.mod.common.battles.dispatch.InstructionSet
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.battle.MoveDTO
import com.cobblemon.mod.common.pokemon.evolution.progress.UseMoveEvolutionProgress
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getPlayer
import java.util.concurrent.CompletableFuture
import net.minecraft.world.entity.LivingEntity
/**
* Format: |move|POKEMON|MOVE|TARGET
@@ -67,6 +71,33 @@ class MoveInstruction(
val actionEffect = actionEffect ?: ActionEffects.actionEffects["generic_move".asIdentifierDefaultingNamespace()]
val targetPokemon = targetPokemon // So smart non-null casts can happen
val players = battle.actors
.map { it.uuid }
.mapNotNull { it.getPlayer() }
.toList()
CobblemonEvents.BATTLE_USE_MOVE.post(BattleUseMoveEvent(battle, players, userPokemon, move))
val moveIndex = if (userPokemon.transformed != null) {
userPokemon.transformed!!.moveSet.getMovesWithNulls().map { it?.name }.indexOf(move.name)
}
else {
userPokemon.moveSet.getMovesWithNulls().map { it?.name }.indexOf(move.name)
}
if (moveIndex == -1) {
Cobblemon.LOGGER.warn("Could not find move ${move.name} on pokemon ${userPokemon.getName()}")
}
else {
if (userPokemon.revealedMoves[moveIndex] == null) {
userPokemon.revealedMoves[moveIndex] = MoveDTO(move.displayName, 1)
}
else {
val update = userPokemon.revealedMoves[moveIndex]!!.timesUsed + 1
userPokemon.revealedMoves[moveIndex]!!.timesUsed = update
}
}
userPokemon.sendUpdate()
battle.notifyAllOfUpdates()
val optionalEffect = message.effect()
ShowdownInterpreter.broadcastOptionalAbility(battle, optionalEffect, userPokemon)
@@ -92,16 +123,17 @@ class MoveInstruction(
}
val lang = when {
userPokemon.effectedPokemon.species.resourceIdentifier.toString().equals("cobblemon:ancientgene") && move.name == "revivalblessing" -> null
optionalEffect?.id == "magicbounce" ->
battleLang("ability.magicbounce", pokemonName, move.displayName)
move.name != "struggle" && spreadTargetPokemon.isEmpty() && targetPokemon != null && targetPokemon != userPokemon && targetPokemon.health > 0 ->
battleLang("used_move_on", pokemonName, move.displayName, targetPokemon.getName())
move.name == "rebirth" ->
battleLang("move.rebirth", pokemonName, extractLastPart(message.rawMessage))
else ->
battleLang("used_move", pokemonName, move.displayName)
}
battle.broadcastChatMessage(lang)
// Notify the actor that one of their Pokemon used a move so they can react (eg. NPC play animation)
userPokemon.actor.onUseMove(userPokemon, targetPokemon)
lang?.let { battle.broadcastChatMessage(it) }
battle.majorBattleActions[userPokemon.uuid] = message
val runtime = MoLangRuntime().also {
@@ -175,4 +207,15 @@ class MoveInstruction(
}
}
}
fun extractLastPart(input: String): String {
val pattern = Regex("\\|([^|]+)\\|([^|]+)$")
val matchResult = pattern.find(input)
return if (matchResult != null) {
val (secondToLast, last) = matchResult.destructured
"$secondToLast and $last"
} else {
""
}
}
}
@@ -17,7 +17,6 @@ import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.pokemon.PokemonSeenEvent
import com.cobblemon.mod.common.api.scheduling.afterOnServer
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.dispatch.*
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
@@ -76,19 +75,24 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
actor.stillSendingOutCount++
battle.sendSidedUpdate(actor, BattleSwitchPokemonPacket(pnx, pokemon, true, illusion), BattleSwitchPokemonPacket(pnx, pokemon, false, illusion))
broadcastSwitch(battle, actor, pokemon, illusion)
afterOnServer(seconds = battleSendoutCount * SEND_OUT_STAGGER_BASE_DURATION + if (battleSendoutCount > 0) Random.nextFloat() * SEND_OUT_STAGGER_RANDOM_MAX_DURATION else 0F ) {
val seconds = battleSendoutCount * SEND_OUT_STAGGER_BASE_DURATION + if (battleSendoutCount > 0) Random.nextFloat() * SEND_OUT_STAGGER_RANDOM_MAX_DURATION else 0F
afterOnServer(seconds = seconds) {
//battle.log("Now sending out ${pokemon.effectedPokemon.getDisplayName()} for ${actor.getName().string}")
pokemon.effectedPokemon.sendOutWithAnimation(
source = entity,
battleId = battle.battleId,
level = entity.level() as ServerLevel,
doCry = false,
position = targetPos,
illusion = illusion?.let { IllusionEffect(it.effectedPokemon) }
source = entity,
battleId = battle.battleId,
level = entity.level() as ServerLevel,
doCry = false,
position = targetPos,
illusion = illusion?.let { IllusionEffect(it.effectedPokemon) }
).thenApply {
actor.stillSendingOutCount--
}
WaitDispatch(0.5F) // we're already waiting 1.5 seconds. this prevents flooding from consecutive SwitchInstructions
activePokemon.battlePokemon?.sendUpdate()
}
return@dispatchToFront WaitDispatch(seconds + 0.25f) // we're already waiting 1.5 seconds. this prevents flooding from consecutive SwitchInstructions
}
}
GO
@@ -131,6 +135,12 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
if (publicMessage.effect()?.id == "batonpass") oldPokemon.contextManager.swap(pokemon.contextManager, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
oldPokemon.contextManager.clear(BattleContext.Type.VOLATILE, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
battle.majorBattleActions[oldPokemon.uuid] = publicMessage
if (oldPokemon.transformed != null) {
for (i in 0 until 4) {
oldPokemon.revealedMoves[i] = null
}
}
oldPokemon.transformed = null
val publicName = (activePokemon.illusion ?: oldPokemon).effectedPokemon.getDisplayName()
actor.sendMessage(battleLang("withdraw.self", publicName))
@@ -141,7 +151,11 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
setOf(
BattleDispatch {
if (entity != null) {
createEntitySwitch(battle, actor, entity, pnx, activePokemon, pokemon, illusion, imposter)
if (battle.isPvP) {
createEntitySwitch(battle, actor, entity, pnx, activePokemon, pokemon, illusion, imposter)
} else {
createEntitySwitchInstant(battle, actor, entity, pnx, activePokemon, pokemon, illusion, imposter)
}
} else {
createNonEntitySwitch(battle, actor, pnx, activePokemon, pokemon, illusion)
}
@@ -166,6 +180,9 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
// If we can't find the entity for some reason then we're going to skip the recall animation
val sendOutFuture = CompletableFuture<Unit>()
(pokemonEntity?.recallWithAnimation() ?: CompletableFuture.completedFuture(Unit)).thenApply {
activePokemon.battlePokemon?.boosts?.clear()
activePokemon.battlePokemon?.sendUpdate()
if (battle.ended) {
sendOutFuture.complete(Unit)
return@thenApply
@@ -186,7 +203,7 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
} else {
// For Singles, we modify the sendout position based on the pokemon's hitbox size
val pos = (if (battle.format.battleType.pokemonPerSide == 1) activePokemon.getSendOutPosition()
else activePokemon.position?.second) ?: entity.position()
else activePokemon.position?.second) ?: entity.position()
// Send out at previous Pokémon's location if it is known, otherwise actor location
val world = entity.level() as ServerLevel
newPokemon.effectedPokemon.sendOutWithAnimation(
@@ -196,7 +213,10 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
position = pos,
doCry = !imposter,
illusion = illusion?.let { IllusionEffect(it.effectedPokemon) }
).thenAccept { sendOutFuture.complete(Unit) }
).thenAccept { entity ->
newPokemon.terastallized?.let { entity.terastallize(it) }
sendOutFuture.complete(Unit)
}
}
broadcastSwitch(battle, actor, newPokemon, illusion)
@@ -205,6 +225,55 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
return UntilDispatch { sendOutFuture.isDone }
}
fun createEntitySwitchInstant(
battle: PokemonBattle,
actor: BattleActor,
entity: LivingEntity,
pnx: String,
activePokemon: ActiveBattlePokemon,
newPokemon: BattlePokemon,
illusion: BattlePokemon? = null,
imposter: Boolean = false
): DispatchResult {
val pokemonEntity = activePokemon.battlePokemon?.entity
pokemonEntity?.pokemon?.recall()
if (!battle.ended) {
activePokemon.battlePokemon?.boosts?.clear()
activePokemon.battlePokemon?.sendUpdate()
// Queue actual swap and send-in after the animation has ended
actor.pokemonList.swap(actor.activePokemon.indexOf(activePokemon), actor.pokemonList.indexOf(newPokemon))
activePokemon.battlePokemon = newPokemon
activePokemon.illusion = illusion
battle.sendSidedUpdate(actor, BattleSwitchPokemonPacket(pnx, newPokemon, true, illusion), BattleSwitchPokemonPacket(pnx, newPokemon, false, illusion))
val newEntity = newPokemon.entity
if (newEntity != null) {
illusion?.let { IllusionEffect(it.effectedPokemon).start(newEntity) }
afterOnServer(seconds = SEND_OUT_DURATION) {
if (!imposter) newPokemon.entity?.cry()
}
} else {
// For Singles, we modify the sendout position based on the pokemon's hitbox size
val pos = (if (battle.format.battleType.pokemonPerSide == 1) activePokemon.getSendOutPosition()
else activePokemon.position?.second) ?: entity.position()
// Send out at previous Pokémon's location if it is known, otherwise actor location
val world = entity.level() as ServerLevel
newPokemon.effectedPokemon.sendOutWithAnimation(
source = entity,
battleId = battle.battleId,
level = world,
position = pos,
doCry = !imposter,
illusion = illusion?.let { IllusionEffect(it.effectedPokemon) }
)
}
broadcastSwitch(battle, actor, newPokemon, illusion)
}
return WaitDispatch(0.25F)
}
fun createNonEntitySwitch(battle: PokemonBattle, actor: BattleActor, pnx: String, activePokemon: ActiveBattlePokemon, newPokemon: BattlePokemon, illusion: BattlePokemon? = null): DispatchResult {
actor.pokemonList.swap(actor.activePokemon.indexOf(activePokemon), actor.pokemonList.indexOf(newPokemon))
activePokemon.battlePokemon = newPokemon
@@ -14,8 +14,10 @@ import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.battles.dispatch.UntilDispatch
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.pokemon.effects.TransformEffect
import com.cobblemon.mod.common.net.messages.client.battle.BattleTransformPokemonPacket
import com.cobblemon.mod.common.net.messages.client.battle.MoveDTO
import com.cobblemon.mod.common.util.battleLang
/**
@@ -58,9 +60,21 @@ class TransformInstruction(val battle: PokemonBattle, val message: BattleMessage
)
}
copyBoosts(pokemon, targetPokemon)
for (i in 0 until 4) {
pokemon.revealedMoves[i] = targetPokemon.moveSet.get(i)?.let { MoveDTO(it.displayName, 0) }
}
pokemon.transformed = targetPokemon
pokemon.revealedAbility = targetPokemon.effectedPokemon.ability.name
pokemon.sendUpdate()
val lang = battleLang("transform", pokemonName, targetPokemonName)
battle.broadcastChatMessage(lang)
battle.minorBattleActions[pokemon.uuid] = message
}
}
private fun copyBoosts(source: BattlePokemon, target: BattlePokemon) {
source.boosts.clear()
target.boosts.forEach { (stat, value) -> source.boosts[stat] = value }
}
}
@@ -45,6 +45,7 @@ class WeatherInstruction(val message: BattleMessage): InterpreterInstruction {
battleLang("weather.$oldWeather.end")
}
}
battle.notifyAllOfUpdates()
battle.broadcastChatMessage(lang)
}
}
@@ -11,26 +11,36 @@ package com.cobblemon.mod.common.battles.pokemon
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asStruct
import com.cobblemon.mod.common.api.moves.MoveSet
import com.cobblemon.mod.common.api.pokemon.feature.BattleFormFeature
import com.cobblemon.mod.common.api.pokemon.helditem.HeldItemManager
import com.cobblemon.mod.common.api.pokemon.helditem.HeldItemProvider
import com.cobblemon.mod.common.api.pokemon.stats.Stat
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.types.tera.TeraType
import com.cobblemon.mod.common.battles.actor.MultiPokemonBattleActor
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.interpreter.ContextManager
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.battle.BattleInitializePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleUpdateTeamPokemonPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorInformationPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattlePokemonDTO
import com.cobblemon.mod.common.net.messages.client.battle.MoveDTO
import com.cobblemon.mod.common.pokemon.IVs
import com.cobblemon.mod.common.pokemon.Nature
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.properties.BattleCloneProperty
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.server
import java.util.UUID
import net.minecraft.network.chat.MutableComponent
import net.minecraft.world.item.ItemStack
import java.util.function.Function
open class BattlePokemon(
@@ -110,9 +120,6 @@ open class BattlePokemon(
/** A set of all the BattlePokemon that they faced during the battle (for exp purposes) */
val facedOpponents = mutableSetOf<BattlePokemon>()
/** The faint sequence number within the battle, or null if this pokemon has not fainted. */
var faintedAt: Int? = null
/**
* The [HeldItemManager] backing this [BattlePokemon].
*/
@@ -120,6 +127,14 @@ open class BattlePokemon(
val contextManager = ContextManager()
val boosts = mutableMapOf<Stats, Int>()
var revealed = false
var revealedAbility: String? = null
val revealedMoves: MutableList<MoveDTO?> = mutableListOf(null, null, null, null)
var revealedHeldItem: ItemStack? = null
var transformed: BattlePokemon? = null
var terastallized: TeraType? = null
open fun getName(): MutableComponent {
val displayPokemon = getIllusion()?.effectedPokemon ?: effectedPokemon
return if (actor is PokemonBattleActor || actor is MultiPokemonBattleActor) {
@@ -129,17 +144,128 @@ open class BattlePokemon(
}
}
fun getBoostMultiplier(stat: Stats): Double {
return when (boosts[stat]) {
-6 -> 2.0 / 8.0
-5 -> 2.0 / 7.0
-4 -> 2.0 / 6.0
-3 -> 2.0 / 5.0
-2 -> 2.0 / 4.0
-1 -> 2.0 / 3.0
0 -> 2.0 / 2.0
1 -> 3.0 / 2.0
2 -> 4.0 / 2.0
3 -> 5.0 / 2.0
4 -> 6.0 / 2.0
5 -> 7.0 / 2.0
6 -> 8.0 / 2.0
else -> 1.0
}
}
fun toBattleDTO(ally: Boolean, forceReveal: Boolean = true): BattlePokemonDTO {
val moves = this.revealedMoves.toMutableList()
for (i in 0 until 4) {
if (moves[i] == null) {
val move = effectedPokemon.moveSet.getMovesWithNulls()[i]
if (move == null) continue
moves[i] = MoveDTO(move.displayName, 0)
}
}
val boostMultipliers = boosts.mapValues { getBoostMultiplier(it.key) }.filter { it.value != 1.0 }
val isTeamSheet = ::actor.isInitialized && actor.battle.format.isOpenTeamSheet
if (ally || isTeamSheet) {
val allyDto = BattleInitializePacket.ActiveBattlePokemonDTO.fromPokemon(this, true, getIllusion())
return BattlePokemonDTO(
this.effectedPokemon.uuid,
this.effectedPokemon.isFainted(),
this.effectedPokemon.ability.name,
moves,
this.effectedPokemon.heldItem,
boostMultipliers,
effectedPokemon.speed,
terastallized = terastallized,
allyDto
)
}
else {
val nonAllyDto = BattleInitializePacket.ActiveBattlePokemonDTO.fromPokemon(this, false, getIllusion())
return BattlePokemonDTO(
this.effectedPokemon.uuid,
this.effectedPokemon.isFainted(),
revealedAbility,
revealedMoves,
revealedHeldItem,
boostMultipliers,
speed = null,
terastallized = terastallized,
if (forceReveal || revealed) nonAllyDto else null
)
}
}
fun updateBattleActorInformation(uuids: List<UUID>) {
uuids.forEach {
if (actor.uuid == it || actor.battle.format.isOpenTeamSheet) {
it.getPlayer()?.sendPacket(
BattleActorInformationPacket(actor.uuid, toBattleDTO(true))
)
}
else {
it.getPlayer()?.sendPacket(
BattleActorInformationPacket(actor.uuid, toBattleDTO(false))
)
}
}
}
fun sendUpdate() {
actor.sendUpdate(BattleUpdateTeamPokemonPacket(effectedPokemon))
this.revealed = true
val uuids = actor.battle.actors.map { it.uuid } + actor.battle.spectators
updateBattleActorInformation(uuids)
}
fun isSentOut() = actor.battle.activePokemon.any { it.battlePokemon == this }
fun canBeSentOut() =
if (actor.request?.side?.pokemon?.any { it.reviving } == true) {
!isSentOut() && !willBeSwitchedIn && health <= 0
fun canBeSentOut(): Boolean {
val reviving = actor.request?.side?.pokemon?.any { it.reviving } == true
println("${actor.getName().string} ${originalPokemon.species.name} canBeSentOut: reviving: $reviving isSentOut: ${isSentOut()} willBeSwitchedIn: $willBeSwitchedIn health: $health")
if (reviving) {
return !isSentOut() && !willBeSwitchedIn && health <= 0
} else {
!isSentOut() && !willBeSwitchedIn && health > 0
return !isSentOut() && !willBeSwitchedIn && health > 0
}
}
fun setBattleFeature(formName: String, enabled: Boolean) {
this.effectedPokemon.getFeature<BattleFormFeature>(formName)?.let {
it.enabled = enabled
this.effectedPokemon.updateAspects()
}
}
fun clearBattleFeatures() {
var modified = false
this.effectedPokemon.features.forEach {
if (it is BattleFormFeature) {
it.enabled = false
modified = true
}
}
fun getIllusion(): BattlePokemon? = this.actor.activePokemon.find { it.battlePokemon == this }?.illusion
if (modified) {
this.effectedPokemon.updateAspects()
}
}
fun getIllusion(): BattlePokemon? {
if (!::actor.isInitialized) return null
return this.actor.activePokemon.find { it.battlePokemon == this }?.illusion
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.battles.timers
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import java.time.Duration
import java.time.Instant
class BasicTimer(override val battle: PokemonBattle, override val actor: PlayerBattleActor, val duration: Duration) : PlayerBattleTimer {
val endTime = Instant.now().plusSeconds(duration.toSeconds())
override fun startTurn() {}
override fun selection() {}
override fun tick() {}
override fun mustChooseBy(): Instant {
return endTime
}
}
@@ -0,0 +1,28 @@
/*
* 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.timers
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.ForfeitActionResponse
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import java.time.Instant
interface PlayerBattleTimer {
val actor: PlayerBattleActor
val battle: PokemonBattle
fun startTurn()
fun selection()
fun tick()
fun mustChooseBy(): Instant
fun timeout() {
actor.setActionResponses(listOf(ForfeitActionResponse()))
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.battles.timers
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.ForfeitActionResponse
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.util.getPlayer
import java.time.Duration
import java.time.Instant
class ShowdownTimer(override val battle: PokemonBattle, override val actor: PlayerBattleActor): PlayerBattleTimer {
private lateinit var turnStart: Instant
private var secondsRemaining = 210
private var hasSelected = false
init {
checkForStreamerIncrease()
}
/**
* For streamers, we introduce a permission that doubles this timer as a deterrence of stream sniping.
* We also have a similar system for notifying streamers when the opponent has used a move (so they can choose last.)
*/
fun checkForStreamerIncrease() {
val player = actor.uuid.getPlayer() ?: return
if (!Cobblemon.permissionValidator.hasPermission(player, "battle.mode.streamer", 4)) return
secondsRemaining *= 2
}
override fun tick() {
if (hasSelected) return
val secondsSinceStart = Duration.between(turnStart, Instant.now()).seconds
if (secondsRemaining - secondsSinceStart <= 0) timeout()
}
override fun selection() {
val secondsSinceStart = Duration.between(turnStart, Instant.now()).seconds.toInt()
secondsRemaining = secondsRemaining - secondsSinceStart + 10
hasSelected = true
}
override fun startTurn() {
turnStart = Instant.now()
hasSelected = false
}
override fun mustChooseBy(): Instant {
return Instant.now().plusSeconds(secondsRemaining.toLong())
}
}
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.api.storage.player.client.ClientTMMoveManager
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.block.entity.TintBlockEntity
import com.cobblemon.mod.common.client.battle.ClientBattle
import com.cobblemon.mod.common.client.battle.ClientBattleInformationRepository
import com.cobblemon.mod.common.client.gui.PartyOverlay
import com.cobblemon.mod.common.client.gui.PartyOverlayDataControl
import com.cobblemon.mod.common.client.gui.RideControlsOverlay
@@ -490,6 +491,7 @@ object CobblemonClient {
fun endBattle() {
battle = null
battleOverlay.lastKnownBattle = null
ClientBattleInformationRepository.mustChooseBy = null
BattleMusicController.endMusic()
}
@@ -38,13 +38,13 @@ class ClientBattle(
knowledge = wildMonSpecies?.let {
//FIXME: Better knowledge checking based on aspects/form and such
CobblemonClient.clientPokedexData.getKnowledgeForSpecies(it)
} ?: PokedexEntryProgress.UNREGISTERED
} ?: PokedexEntryProgress.NONE
}
else {
knowledge = PokedexEntryProgress.UNREGISTERED
knowledge = PokedexEntryProgress.NONE
}
}
var knowledge = PokedexEntryProgress.UNREGISTERED
var knowledge = PokedexEntryProgress.NONE
val sides: Array<ClientBattleSide>
get() = arrayOf(side1, side2)
@@ -84,6 +84,7 @@ class ClientBattle(
pendingActionRequests.map { it.response!! }
)
)
ClientBattleInformationRepository.mustChooseBy = null
mustChoose = false
}
}
@@ -0,0 +1,21 @@
/*
* 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.battle
import com.cobblemon.mod.common.net.messages.client.battle.BattleInformationDTO
import com.cobblemon.mod.common.net.messages.client.battle.BattlePokemonDTO
import java.time.Instant
import java.util.UUID
object ClientBattleInformationRepository {
val actors = mutableMapOf<UUID, MutableList<BattlePokemonDTO>>()
val battles = mutableMapOf<UUID, BattleInformationDTO>()
val actorActualSides = mutableMapOf<UUID, Int>()
var mustChooseBy: Instant? = null
}
@@ -8,7 +8,7 @@
package com.cobblemon.mod.common.client.battle
import net.minecraft.util.FormattedCharSequence
import net.minecraft.network.chat.Component
/**
* A wrapper around the client battle message information. This is essentially an appending list of all
@@ -19,15 +19,15 @@ import net.minecraft.util.FormattedCharSequence
* @since June 24th, 2022
*/
class ClientBattleMessageQueue {
var listeners = mutableListOf<(FormattedCharSequence) -> Unit>()
private val messages = mutableListOf<FormattedCharSequence>()
var listeners = mutableListOf<(Component) -> Unit>()
private val messages = mutableListOf<Component>()
fun add(messages: Iterable<FormattedCharSequence>) {
fun add(messages: Iterable<Component>) {
this.messages.addAll(messages)
listeners.forEach { listener -> messages.forEach(listener) }
}
fun subscribe(listener: (FormattedCharSequence) -> Unit) {
fun subscribe(listener: (Component) -> Unit) {
this.listeners.add(listener)
messages.forEach(listener)
}
@@ -0,0 +1,120 @@
/*
* 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.battle
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.ElementalTypes
object MoveEffectivenessCalculator {
fun getMoveEffectiveness(attack: ElementalType, defenderType1: ElementalType, defenderType2: ElementalType?): Effectiveness {
val type1Effectiveness = getEffectiveness(attack, defenderType1)
val type2Effectiveness = if (defenderType2 == null) 1f else getEffectiveness(attack, defenderType2)
val typeEffectiveness = type1Effectiveness * type2Effectiveness
return when {
typeEffectiveness == 0f -> Effectiveness.IMMUNE
typeEffectiveness == 0.25f -> Effectiveness.MOSTLY_INEFFECTIVE
typeEffectiveness == 0.5f -> Effectiveness.NOT_VERY_EFFECTIVE
typeEffectiveness == 1f -> Effectiveness.EFFECTIVE
typeEffectiveness == 2f -> Effectiveness.SUPER_EFFECTIVE
typeEffectiveness == 4f -> Effectiveness.EXTREMELY_EFFECTIVE
else -> Effectiveness.IMMUNE
}
}
fun getEffectiveness(attack: ElementalType, defender: ElementalType): Float {
if (isImmune(attack, defender)) return 0f
if (isSuperEffective(attack, defender)) return 2f
if (isNotVeryEffective(attack, defender)) return 0.5f
return 1f
}
fun isImmune(attack: ElementalType, defender: ElementalType): Boolean {
when (attack) {
ElementalTypes.NORMAL -> return defender == ElementalTypes.GHOST
ElementalTypes.FIRE -> return false
ElementalTypes.WATER -> return false
ElementalTypes.GRASS -> return false
ElementalTypes.ELECTRIC -> return defender == ElementalTypes.GROUND
ElementalTypes.ICE -> return false
ElementalTypes.FIGHTING -> return defender == ElementalTypes.GHOST
ElementalTypes.POISON -> return defender == ElementalTypes.STEEL
ElementalTypes.GROUND -> return defender == ElementalTypes.FLYING
ElementalTypes.FLYING -> return false
ElementalTypes.PSYCHIC -> return defender == ElementalTypes.DARK
ElementalTypes.BUG -> return false
ElementalTypes.ROCK -> return false
ElementalTypes.GHOST -> return defender == ElementalTypes.NORMAL
ElementalTypes.DRAGON -> return defender == ElementalTypes.FAIRY
ElementalTypes.DARK -> return false
ElementalTypes.STEEL -> return false
ElementalTypes.FAIRY -> return false
}
return false
}
fun isSuperEffective(attack: ElementalType, defender: ElementalType): Boolean {
when (attack) {
ElementalTypes.NORMAL -> return false
ElementalTypes.FIRE -> return defender in listOf(ElementalTypes.GRASS, ElementalTypes.ICE, ElementalTypes.BUG, ElementalTypes.STEEL)
ElementalTypes.WATER -> return defender in listOf(ElementalTypes.FIRE, ElementalTypes.GROUND, ElementalTypes.ROCK)
ElementalTypes.GRASS -> return defender in listOf(ElementalTypes.WATER, ElementalTypes.GROUND, ElementalTypes.ROCK)
ElementalTypes.ELECTRIC -> return defender in listOf(ElementalTypes.WATER, ElementalTypes.FLYING)
ElementalTypes.ICE -> return defender in listOf(ElementalTypes.GRASS, ElementalTypes.GROUND, ElementalTypes.FLYING, ElementalTypes.DRAGON)
ElementalTypes.FIGHTING -> return defender in listOf(ElementalTypes.NORMAL, ElementalTypes.ICE, ElementalTypes.ROCK, ElementalTypes.DARK, ElementalTypes.STEEL)
ElementalTypes.POISON -> return defender in listOf(ElementalTypes.GRASS, ElementalTypes.FAIRY)
ElementalTypes.GROUND -> return defender in listOf(ElementalTypes.FIRE, ElementalTypes.ELECTRIC, ElementalTypes.POISON, ElementalTypes.ROCK, ElementalTypes.STEEL)
ElementalTypes.FLYING -> return defender in listOf(ElementalTypes.GRASS, ElementalTypes.FIGHTING, ElementalTypes.BUG)
ElementalTypes.PSYCHIC -> return defender in listOf(ElementalTypes.FIGHTING, ElementalTypes.POISON)
ElementalTypes.BUG -> return defender in listOf(ElementalTypes.GRASS, ElementalTypes.PSYCHIC, ElementalTypes.DARK)
ElementalTypes.ROCK -> return defender in listOf(ElementalTypes.FIRE, ElementalTypes.ICE, ElementalTypes.FLYING, ElementalTypes.BUG)
ElementalTypes.GHOST -> return defender in listOf(ElementalTypes.PSYCHIC, ElementalTypes.GHOST)
ElementalTypes.DRAGON -> return defender in listOf(ElementalTypes.DRAGON)
ElementalTypes.DARK -> return defender in listOf(ElementalTypes.PSYCHIC, ElementalTypes.GHOST)
ElementalTypes.STEEL -> return defender in listOf(ElementalTypes.ICE, ElementalTypes.ROCK, ElementalTypes.FAIRY)
ElementalTypes.FAIRY -> return defender in listOf(ElementalTypes.FIGHTING, ElementalTypes.DRAGON, ElementalTypes.DARK)
}
return false
}
fun isNotVeryEffective(attack: ElementalType, defender: ElementalType): Boolean {
when (attack) {
ElementalTypes.NORMAL -> return defender in listOf(ElementalTypes.ROCK, ElementalTypes.STEEL)
ElementalTypes.FIRE -> return defender in listOf(ElementalTypes.FIRE, ElementalTypes.WATER, ElementalTypes.ROCK, ElementalTypes.DRAGON)
ElementalTypes.WATER -> return defender in listOf(ElementalTypes.WATER, ElementalTypes.GRASS, ElementalTypes.DRAGON)
ElementalTypes.GRASS -> return defender in listOf(ElementalTypes.FIRE, ElementalTypes.GRASS, ElementalTypes.POISON, ElementalTypes.FLYING, ElementalTypes.BUG, ElementalTypes.DRAGON, ElementalTypes.STEEL)
ElementalTypes.ELECTRIC -> return defender in listOf(ElementalTypes.GRASS, ElementalTypes.ELECTRIC, ElementalTypes.DRAGON)
ElementalTypes.ICE -> return defender in listOf(ElementalTypes.FIRE, ElementalTypes.WATER, ElementalTypes.ICE, ElementalTypes.STEEL)
ElementalTypes.FIGHTING -> return defender in listOf(ElementalTypes.POISON, ElementalTypes.FLYING, ElementalTypes.PSYCHIC, ElementalTypes.BUG, ElementalTypes.FAIRY)
ElementalTypes.POISON -> return defender in listOf(ElementalTypes.POISON, ElementalTypes.GROUND, ElementalTypes.ROCK, ElementalTypes.GHOST)
ElementalTypes.GROUND -> return defender in listOf(ElementalTypes.GRASS, ElementalTypes.BUG)
ElementalTypes.FLYING -> return defender in listOf(ElementalTypes.ELECTRIC, ElementalTypes.ROCK, ElementalTypes.STEEL)
ElementalTypes.PSYCHIC -> return defender in listOf(ElementalTypes.PSYCHIC, ElementalTypes.STEEL)
ElementalTypes.BUG -> return defender in listOf(ElementalTypes.FIRE, ElementalTypes.FIGHTING, ElementalTypes.POISON, ElementalTypes.FLYING, ElementalTypes.GHOST, ElementalTypes.STEEL, ElementalTypes.FAIRY)
ElementalTypes.ROCK -> return defender in listOf(ElementalTypes.FIGHTING, ElementalTypes.GROUND, ElementalTypes.STEEL)
ElementalTypes.GHOST -> return defender in listOf(ElementalTypes.DARK)
ElementalTypes.DRAGON -> return defender in listOf(ElementalTypes.STEEL)
ElementalTypes.DARK -> return defender in listOf(ElementalTypes.FIGHTING, ElementalTypes.DARK, ElementalTypes.FAIRY)
ElementalTypes.STEEL -> return defender in listOf(ElementalTypes.FIRE, ElementalTypes.WATER, ElementalTypes.ELECTRIC, ElementalTypes.STEEL)
ElementalTypes.FAIRY -> return defender in listOf(ElementalTypes.FIRE, ElementalTypes.POISON, ElementalTypes.STEEL)
}
return false
}
}
enum class Effectiveness {
EXTREMELY_EFFECTIVE,
SUPER_EFFECTIVE,
EFFECTIVE,
NOT_VERY_EFFECTIVE,
MOSTLY_INEFFECTIVE,
IMMUNE
}
@@ -0,0 +1,41 @@
/*
* 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.battle.preview
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.net.messages.client.battle.BattlePokemonDTO
import com.cobblemon.mod.common.net.messages.server.battle.TeamPreviewSelectPokemonPacket
import net.minecraft.network.chat.Component
import java.time.Instant
class ClientBattleTeamPreview(
val selections: Int,
val team: List<BattlePokemonDTO?>,
val opponent: List<BattlePokemonDTO?>,
val mustPickBy: Instant,
val opponentName: Component
) {
val selection = mutableListOf<BattlePokemonDTO>()
fun select(pokemon: BattlePokemonDTO) {
selection.add(pokemon)
sendChangePacket()
}
fun unselect(pokemon: BattlePokemonDTO) {
selection.remove(pokemon)
sendChangePacket()
}
private fun sendChangePacket() {
val indexes = selection.map { team.indexOf(it) }
if (indexes.any { it == -1 }) return
CobblemonNetwork.sendToServer(TeamPreviewSelectPokemonPacket(indexes.toSet()))
}
}
@@ -16,13 +16,14 @@ import net.minecraft.client.gui.GuiGraphics
class TypeIcon(
val x: Number,
val y: Number,
val type: ElementalType,
val type: ElementalType? = null,
val secondaryType: ElementalType? = null,
val centeredX: Boolean = false,
val small: Boolean = false,
val secondaryOffset: Float = 15F,
val doubleCenteredOffset: Float = 7.5F,
val opacity: Float = 1F
val opacity: Float = 1F,
val isStellar: Boolean = false // TODO: Make this less hacky, make type & secondaryType take tera types instead of elemental types
) {
companion object {
private const val TYPE_ICON_DIAMETER = 36
@@ -30,6 +31,7 @@ class TypeIcon(
private val typesResource = cobblemonResource("textures/gui/types.png")
private val smallTypesResource = cobblemonResource("textures/gui/types_small.png")
val stellarType = cobblemonResource("textures/gui/battle/stellar_type.png")
}
fun render(context: GuiGraphics) {
@@ -51,17 +53,32 @@ class TypeIcon(
)
}
blitk(
matrixStack = context.pose(),
texture = if (small) smallTypesResource else typesResource,
x = (x.toFloat() - offsetX) / SCALE,
y = y.toFloat() / SCALE,
height = diameter,
width = diameter,
uOffset = diameter * type.textureXMultiplier.toFloat() + 0.1,
textureWidth = diameter * 18,
alpha = opacity,
scale = SCALE
)
if (type != null && !isStellar) {
blitk(
matrixStack = context.pose(),
texture = if (small) smallTypesResource else typesResource,
x = (x.toFloat() - offsetX) / SCALE,
y = y.toFloat() / SCALE,
height = diameter,
width = diameter,
uOffset = diameter * type.textureXMultiplier.toFloat() + 0.1,
textureWidth = diameter * 18,
alpha = opacity,
scale = SCALE
)
}
else {
blitk(
matrixStack = context.pose(),
texture = stellarType,
x = (x.toFloat() - offsetX) / SCALE,
y = y.toFloat() / SCALE,
height = 36,
width = 36,
textureWidth = 36,
alpha = opacity,
scale = SCALE
)
}
}
}
@@ -18,12 +18,15 @@ import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleActionSelectio
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleBackButton
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleGeneralActionSelection
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleSwitchPokemonSelection
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleTeamInfoSelection
import com.cobblemon.mod.common.client.gui.battle.subscreen.ForfeitConfirmationSelection
import com.cobblemon.mod.common.client.gui.battle.widgets.BattleMessagePane
import com.cobblemon.mod.common.client.gui.battle.widgets.BattleOptionTile
import com.cobblemon.mod.common.client.keybind.boundKey
import com.cobblemon.mod.common.client.keybind.keybinds.PartySendBinding
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.server.battle.RemoveSpectatorPacket
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.Minecraft
@@ -44,10 +47,18 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
val forfeitResource = cobblemonResource("textures/gui/battle/battle_menu_forfeit.png")
}
private lateinit var messagePane: BattleMessagePane
lateinit var messagePane: BattleMessagePane
var opacity = 0F
val actor = CobblemonClient.battle?.side1?.actors?.find { it.uuid == Minecraft.getInstance().player?.uuid }
val specBackButton = BattleBackButton(12f, Minecraft.getInstance().window.guiScaledHeight - 32f)
val specBackButton = BattleBackButton(OPTION_ROOT_X - 3F, Minecraft.getInstance().window.guiScaledHeight - 22F)
val teamInfoButton = BattleOptionTile(
battleGUI = this,
x = OPTION_ROOT_X,
y = Minecraft.getInstance().window.guiScaledHeight - OPTION_VERTICAL_OFFSET + BattleOptionTile.OPTION_HEIGHT + OPTION_HORIZONTAL_SPACING,
resource = BattleGeneralActionSelection.battleInfoSelection,
text = "cobblemon.battle.ui.team_info.button".asTranslated(),
onClick = {}
)
var queuedActions = mutableListOf<() -> Unit>()
@@ -116,21 +127,13 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
}
}
if (battle.spectating) {
if (battle.spectating && !BattleTeamInfoSelection.visible) {
specBackButton.render(context, mouseX, mouseY, delta)
teamInfoButton.render(context, mouseX, mouseY, delta)
}
val currentSelection = getCurrentActionSelection()
if (currentSelection == null || currentSelection is BattleGeneralActionSelection ) {
drawScaledText(
context = context,
text = battleLang("ui.hide_label", PartySendBinding.boundKey().displayName),
x = Minecraft.getInstance().window.guiScaledWidth / 2,
y = (Minecraft.getInstance().window.guiScaledHeight / 5),
opacity = 0.75F * opacity,
centered = true
)
} else if (currentSelection is ForfeitConfirmationSelection) {
if (currentSelection is ForfeitConfirmationSelection) {
drawScaledText(
context = context,
text = battleLang("ui.forfeit_confirmation", PartySendBinding.boundKey().displayName),
@@ -144,6 +147,8 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
queuedActions.forEach { it() }
queuedActions.clear()
CobblemonClient.battleOverlay.mouseX = mouseX
CobblemonClient.battleOverlay.mouseY = mouseY
}
override fun renderBackground(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
@@ -151,8 +156,6 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
}
fun deriveRootActionSelection(actor: ClientBattleActor, request: SingleActionRequest): BattleActionSelection? {
return if (request.forceSwitch) {
BattleSwitchPokemonSelection(this, request)
} else {
@@ -173,6 +176,7 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
override fun onClose() {
super.onClose()
CobblemonClient.battle?.minimised = true
BattleTeamInfoSelection.visible = false
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
@@ -193,9 +197,15 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
}
val battle = CobblemonClient.battle
if (battle?.spectating == true && specBackButton.isHovered(mouseX, mouseY)) {
RemoveSpectatorPacket(battle.battleId).sendToServer()
CobblemonClient.endBattle()
if (battle?.spectating == true && !BattleTeamInfoSelection.visible) {
if (specBackButton.isHovered(mouseX, mouseY)) {
RemoveSpectatorPacket(battle.battleId).sendToServer()
CobblemonClient.endBattle()
}
if (teamInfoButton.isHovered(mouseX, mouseY)) {
changeActionSelection(BattleTeamInfoSelection(this))
BattleTeamInfoSelection.visible = true
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.battle.preview
import com.cobblemon.mod.common.client.battle.preview.ClientBattleTeamPreview
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.util.battleLang
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.Screen
class TeamPreviewGUI(val preview: ClientBattleTeamPreview) : Screen(battleLang("gui.team_preview")), CobblemonRenderable {
private lateinit var widget: TeamPreviewWidget
override fun init() {
super.init()
widget = addRenderableWidget(TeamPreviewWidget(preview))
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
super.render(guiGraphics, mouseX, mouseY, partialTick)
}
override fun shouldCloseOnEsc() = false
override fun isPauseScreen() = false
}
@@ -0,0 +1,973 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.battle.preview
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.gui.ParentWidget
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.renderSprite
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.font
import com.cobblemon.mod.common.api.text.green
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.battle.ClientBattleInformationRepository
import com.cobblemon.mod.common.client.battle.ClientBattlePokemon
import com.cobblemon.mod.common.client.battle.preview.ClientBattleTeamPreview
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay.Companion.PORTRAIT_DIAMETER
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay.Companion.SCALE
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay.Companion.questionMarkIcon
import com.cobblemon.mod.common.client.render.SpriteType
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.drawScaledTextJustifiedRight
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.net.messages.client.battle.BattlePokemonDTO
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.toHex
import com.mojang.blaze3d.platform.InputConstants
import com.mojang.blaze3d.platform.Lighting
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.math.Axis
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.narration.NarratableEntry
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.client.sounds.SoundManager
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import org.joml.Quaternionf
import org.joml.Vector3f
import java.math.RoundingMode
import java.text.DecimalFormat
import java.time.Duration
import java.time.Instant
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.max
class TeamPreviewWidget(val preview: ClientBattleTeamPreview) : ParentWidget(
pX = 0,
pY = if (Minecraft.getInstance().window.guiScaledHeight > 304) (Minecraft.getInstance().window.guiScaledHeight / 2) - (BACKGROUND_HEIGHT / 2)
else Minecraft.getInstance().window.guiScaledHeight - (BACKGROUND_HEIGHT + 78),
pWidth = Minecraft.getInstance().window.guiScaledWidth,
pHeight = Minecraft.getInstance().window.guiScaledHeight,
"cobblemon.battle.ui.team_info".asTranslated()
) {
companion object {
const val SLOT_HORIZONTAL_SPACING = 4F
const val SLOT_VERTICAL_SPACING = 2F
const val BACKGROUND_HEIGHT = 148
val underlayTexture = cobblemonResource("textures/gui/battle/selection_underlay.png")
val title = cobblemonResource("textures/gui/battle/team_preview_title.png")
val checkmark = cobblemonResource("textures/gui/battle/checkmark.png")
var visible = false
}
val teamTiles = mutableListOf<PokemonTile>()
val opponentTiles = mutableListOf<PokemonTile>()
init {
addTiles()
}
fun addTiles() {
preview.team.forEachIndexed { index, pokemon ->
if (pokemon == null) return@forEachIndexed
val (slotX, slotY) = getSlotPosition(index, 1)
teamTiles.add(PokemonTile(slotX, slotY, pokemon, this, false))
}
preview.opponent.forEachIndexed { index, pokemon ->
if (pokemon == null) return@forEachIndexed
val (slotX, slotY) = getSlotPosition(index, 2)
opponentTiles.add(PokemonTile(slotX, slotY, pokemon, this, true))
}
}
fun getSlotPosition(index: Int, side: Int): Pair<Float, Float> {
val startX = if (side == 1) 10 else width - 10 - (41 + SLOT_HORIZONTAL_SPACING) * 2
val startY = y + 15
val row = index / 2
val column = index % 2
val slotX = startX.toFloat() + column * (SLOT_HORIZONTAL_SPACING + 41)
val slotY = startY.toFloat() + row * (SLOT_VERTICAL_SPACING + 39)
return Pair(slotX, slotY)
}
private fun getHoveredTile(team: Boolean, mouseX: Int, mouseY: Int): Int {
val tiles = if (team) teamTiles else opponentTiles
return tiles.indexOfFirst { it.isHovered(mouseX.toDouble(), mouseY.toDouble()) }
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrixStack = context.pose()
val mc = Minecraft.getInstance()
blitk(
matrixStack = matrixStack,
texture = underlayTexture,
x = x,
y = y,
width = width,
height = BACKGROUND_HEIGHT
)
blitk(
matrixStack = matrixStack,
texture = title,
x = (mc.window.guiScaledWidth / 2) - (103 / 2),
y = 4,
width = 103,
height = 22
)
drawScaledText(
context = context,
text = "cobblemon.battle.ui.team_preview.title".text().bold(),
x = (mc.window.guiScaledWidth / 2),
y = 6,
scale = 0.75f,
shadow = true,
centered = true
)
val totalSecondsRemaining = Duration.between(Instant.now(), preview.mustPickBy).seconds
val minutesRemaining = max(0, totalSecondsRemaining / 60)
val secondsRemaining = max(0, totalSecondsRemaining % 60)
val secondsRemainingStr = String.format("%02d", secondsRemaining)
val timer = "$minutesRemaining:$secondsRemainingStr"
val text = when {
totalSecondsRemaining <= 15 -> timer.text().red().bold()
totalSecondsRemaining <= 30 -> timer.text().yellow().bold()
else -> timer.text().bold()
}
drawScaledText(
context = context,
text = text,
x = (mc.window.guiScaledWidth / 2) + 2,
y = 15,
scale = 0.75f,
shadow = true,
centered = true
)
val ellipsis = ".".repeat(3 - (secondsRemaining % 3).toInt())
if (preview.selection.size == preview.selections) {
blitk(
matrixStack = matrixStack,
texture = checkmark,
x = (mc.window.guiScaledWidth / 2) - 39,
y = 24,
width = 8,
height = 6
)
val offset = when (ellipsis.length) {
1 -> 0.5
2 -> 1.0
else -> 2.0
}
drawScaledText(
context = context,
text = "cobblemon.battle.ui.team_preview.waiting".asTranslated(ellipsis).bold(),
x = (mc.window.guiScaledWidth / 2) + 2.0 + offset,
y = 26,
scale = 0.5f,
shadow = true,
centered = true
)
}
blitk(
matrixStack = matrixStack,
texture = cobblemonResource("textures/gui/battle/name_plate.png"),
x = 10,
y = y - 8,
width = 128,
height = 8
)
drawScaledText(
context = context,
text = mc.player!!.name.string.text(),
y = y - 8 + 2,
x = x + 14 + 10,
scale = SCALE,
shadow = true
)
blitk(
matrixStack = matrixStack,
texture = cobblemonResource("textures/gui/battle/nameplate_reversed.png"),
x = mc.window.guiScaledWidth - 14 - 128,
y = y - 8,
width = 128,
height = 8
)
drawScaledTextJustifiedRight(
context = context,
text = preview.opponentName.string.text(),
y = y - 8 + 2,
x = mc.window.guiScaledWidth - 14 - 12,
scale = SCALE,
shadow = true
)
teamTiles.forEachIndexed { index, tile ->
val hoveredTile = getHoveredTile(true, mouseX, mouseY)
if (shouldHide(hoveredTile, index, false)) return@forEachIndexed
tile.render(context, mouseX.toDouble(), mouseY.toDouble(), delta)
}
opponentTiles.forEachIndexed { index, tile ->
val hoveredTile = getHoveredTile(false, mouseX, mouseY)
if (shouldHide(hoveredTile, index, true)) return@forEachIndexed
tile.render(context, mouseX.toDouble(), mouseY.toDouble(), delta)
}
}
private fun shouldHide(hoveredTile: Int, currentTile: Int, reversed: Boolean): Boolean {
if (hoveredTile != -1) {
val hoveredRow = hoveredTile / 2
val hoveredColumn = hoveredTile % 2
val currentRow = currentTile / 2
val currentColumn = currentTile % 2
if (!reversed && hoveredColumn < currentColumn && hoveredRow <= currentRow) return true
if (reversed && hoveredColumn > currentColumn && hoveredRow <= currentRow) return true
}
return false
}
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
val tile = teamTiles.firstOrNull { it.isHovered(pMouseX, pMouseY) }
if (tile != null && tile.pokemonDto in preview.team) {
if (pButton == InputConstants.MOUSE_BUTTON_LEFT) {
if (tile.pokemonDto !in preview.selection && preview.selection.size < preview.selections) {
preview.select(tile.pokemonDto)
}
}
else if (pButton == InputConstants.MOUSE_BUTTON_RIGHT) {
if (tile.pokemonDto in preview.selection) {
preview.unselect(tile.pokemonDto)
}
}
}
playDownSound(Minecraft.getInstance().soundManager)
return super.mouseClicked(pMouseX, pMouseY, pButton)
}
override fun defaultButtonNarrationText(builder: NarrationElementOutput) {
}
override fun playDownSound(soundManager: SoundManager) {
soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
}
override fun narrationPriority() = NarratableEntry.NarrationPriority.HOVERED
class PokemonTile(
val x: Float,
val y: Float,
val pokemonDto: BattlePokemonDTO,
val widget: TeamPreviewWidget,
val reversed: Boolean
) {
companion object {
const val SELECT_WIDTH = 41
const val SELECT_HEIGHT = 39
const val SCALE = 0.5F
val expandedPokemonInfo = cobblemonResource("textures/gui/battle/expanded_pokemon_info_center.png")
val pokemonTile = cobblemonResource("textures/gui/battle/pokemon_tile.png")
val pokemonTileReversed = cobblemonResource("textures/gui/battle/pokemon_tile_reversed.png")
val pokemonTileDisabled = cobblemonResource("textures/gui/battle/pokemon_tile_disabled.png")
val pokemonTileDisabledReversed = cobblemonResource("textures/gui/battle/pokemon_tile_disabled_reversed.png")
val pokemonTileSelected = cobblemonResource("textures/gui/battle/pokemon_tile_selected.png")
val pokemonTileSelectedReversed = cobblemonResource("textures/gui/battle/pokemon_tile_selected_reversed.png")
val hoverGapSelected = cobblemonResource("textures/gui/battle/hover_gap_selected.png")
val hoverGap = cobblemonResource("textures/gui/battle/hover_gap.png")
val hoverGapReversed = cobblemonResource("textures/gui/battle/hover_gap_reversed.png")
private val decimalFormat = DecimalFormat("0.00").also {
it.roundingMode = RoundingMode.CEILING
}
}
val state = FloatingState()
fun isHovered(mouseX: Double, mouseY: Double) = mouseX in x..(x + SELECT_WIDTH) && mouseY in (y..(y + SELECT_HEIGHT))
fun render(context: GuiGraphics, mouseX: Double, mouseY: Double, deltaTicks: Float) {
val pokemon = pokemonDto.activeBattlePokemonDTO ?: return
val battlePokemon = ClientBattlePokemon(
uuid = pokemon.uuid,
properties = pokemon.properties,
aspects = pokemon.aspects,
displayName = pokemon.displayName,
hpValue = pokemon.hpValue,
maxHp = pokemon.maxHp,
isHpFlat = pokemon.isFlatHp,
status = pokemon.status,
statChanges = pokemon.statChanges,
)
state.currentAspects = pokemon.aspects
val matrixStack = context.pose()
val isFainted = pokemonDto.fainted
val isSelected = pokemonDto in widget.preview.selection
blitk(
matrixStack = matrixStack,
texture = cobblemonResource("textures/gui/battle/battle_info_underlay.png"),
x = x + 5 + (if (reversed) 3 else 0),
y = y + 4,
width = 28,
height = 28
)
val speciesToDisplay = battlePokemon.species
val stateToDisplay = battlePokemon.state
context.enableScissor(
(x + 5 + (if (reversed) 3 else 0)).toInt(),
(y + 5).toInt(),
(x + 5 + 28 + (if (reversed) 3 else 0)).toInt(),
(y + 5 + 28).toInt(),
)
matrixStack.pushPose()
matrixStack.translate(
x + (if (reversed) 3 else 0) + 5 + 28 / 2.0,
y.toDouble() + 5 - 5.0,
0.0
)
drawCustomPosablePortrait(
identifier = speciesToDisplay.resourceIdentifier,
matrixStack = matrixStack,
scale = 18f,
contextScale = speciesToDisplay.getForm(stateToDisplay.currentAspects).baseScale,
reversed = reversed,
doQuirks = false,
state = stateToDisplay,
partialTicks = if (Cobblemon.config.animateBattleTiles) deltaTicks else 0F
)
matrixStack.popPose()
context.disableScissor()
val tile = if (reversed) pokemonTileReversed else pokemonTile
val disabled = if (reversed) pokemonTileDisabledReversed else pokemonTileDisabled
val selected = if (reversed) pokemonTileSelectedReversed else pokemonTileSelected
if (isFainted) {
blitk(
matrixStack = matrixStack,
texture = disabled,
x = x,
y = y,
width = 41,
height = 39
)
}
else if (isSelected) {
blitk(
matrixStack = matrixStack,
texture = selected,
x = x,
y = y,
width = 41,
height = 39
)
val selectionIndex = widget.preview.selection.indexOf(pokemonDto)
if (selectionIndex != -1) {
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = "${selectionIndex + 1}".text().bold(),
x = x + 34,
y = y + 29,
scale = 1.25f,
opacity = 1f,
shadow = true
)
}
}
else {
blitk(
matrixStack = matrixStack,
texture = tile,
x = x,
y = y,
width = 41,
height = 39
)
}
if (isHovered(mouseX, mouseY)) {
renderExpandedInfo(context, isSelected, reversed, battlePokemon, pokemonDto)
}
}
private fun renderExpandedInfo(context: GuiGraphics, selected: Boolean, reversed: Boolean, pokemon: ClientBattlePokemon, dto: BattlePokemonDTO) {
context.pose().pushPose()
context.pose().translate(0.0, 0.0, 300.0)
val startX = if (reversed) x - 136 else x + 40
val startY = y
val species = pokemon.species
val form = species.getForm(pokemon.state.currentAspects)
val expandedInfoTexture = expandedPokemonInfo
if (selected) {
blitk(
matrixStack = context.pose(),
texture = hoverGapSelected,
x = startX - 3,
y = startY,
height = 30,
width = 4,
textureHeight = 30,
textureWidth = 4,
)
}
else {
val gap = if (reversed) hoverGapReversed else hoverGap
val xOffset = if (reversed) 136 else -7
blitk(
matrixStack = context.pose(),
texture = gap,
x = startX + xOffset,
y = startY,
height = 36,
width = 8,
textureHeight = 36,
textureWidth = 8,
)
}
blitk(
matrixStack = context.pose(),
texture = expandedInfoTexture,
x = startX,
y = startY,
height = 112,
width = 137,
textureHeight = 112,
textureWidth = 137,
)
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.pokemon_form".asTranslated().bold(),
x = startX + 36,
y = startY + 6.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = getFormText(species, form),
x = startX + 36,
y = startY + 13.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.buffs".asTranslated().bold(),
x = startX + 36,
y = startY + 23.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = "cobblemon.stat.attack.name".asTranslated(),
x = startX + 10,
y = startY + 31.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.ATTACK)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 31.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.defence.name".asTranslated(),
x = startX + 10,
y = startY + 39.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.DEFENCE)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 39.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.battle.ui.stats.special_attack".asTranslated(),
x = startX + 10,
y = startY + 47.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.SPECIAL_ATTACK)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 47.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.battle.ui.stats.special_defence".asTranslated(),
x = startX + 10,
y = startY + 55.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.SPECIAL_DEFENCE)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 55.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.speed.name".asTranslated(),
x = startX + 10,
y = startY + 63.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.SPEED)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 63.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.accuracy.name".asTranslated(),
x = startX + 10,
y = startY + 71.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.ACCURACY)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 71.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.evasion.name".asTranslated(),
x = startX + 10,
y = startY + 79.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto?.buffs?.get(Stats.EVASION)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 79.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.ability".asTranslated().bold(),
x = startX + 100,
y = startY + 6.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
val ability = dto.ability?.let { Abilities.get(it) }
val abilityName = ability?.displayName?.text() ?: "?".text()
drawScaledText(
context = context,
text = abilityName,
x = startX + 100,
y = startY + 13.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.moves".asTranslated().bold(),
x = startX + 100,
y = startY + 23.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
if (dto.moves.size != 4) {
for (i in 0 until 4) {
drawScaledText(
context = context,
text = "?".text(),
x = startX + 100,
y = startY + 31.5 + i * 8,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
}
}
else {
val questionMarkText = "?".text()
dto.moves.forEachIndexed { index, moveDTO ->
val move = moveDTO?.move ?: questionMarkText
drawScaledText(
context = context,
text = move.string.text(),
x = if (move == questionMarkText) startX + 100 else startX + 74,
y = startY + 31.5 + index * 8,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = move == questionMarkText
)
if (moveDTO != null) {
drawScaledTextJustifiedRight(
context = context,
text = moveDTO.timesUsed.toString().text(),
x = startX + 127.5,
y = startY + 31.5 + index * 8,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
)
}
}
}
if (dto.speed != null) {
val speed = dto.speed
val speedBoost = dto.buffs.get(Stats.SPEED) ?: 1.0
val speedAfterBoost = (speed * speedBoost).toInt()
drawScaledText(
context = context,
text = "cobblemon.stat.speed.name".asTranslated(),
x = startX + 100,
y = startY + 65.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = speedAfterBoost.toString().text(),
x = startX + 100,
y = startY + 72.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
}
else {
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.speed_tier".asTranslated(),
x = startX + 100,
y = startY + 65.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
val speedBoost = dto.buffs.get(Stats.SPEED) ?: 1.0
val speedTier = getSpeedRange(form, pokemon.level, speedBoost)
drawScaledText(
context = context,
text = speedTier,
x = startX + 100,
y = startY + 72.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.held_item".asTranslated(),
x = startX + 40,
y = startY + 99.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
TypeIcon(
x = startX + 100.5,
y = startY + 88,
type = form.primaryType,
secondaryType = form.secondaryType,
small = false,
centeredX = true
).render(context)
if (form.secondaryType != null) {
blitk(
matrixStack = context.pose(),
texture = cobblemonResource("textures/gui/battle/type_spacer_double.png"),
x = (startX + 69.0) / 0.45,
y = (startY + 91.5) / 0.45,
height = 24,
width = 140,
textureHeight = 24,
textureWidth = 140,
scale = 0.45f
)
}
else {
blitk(
matrixStack = context.pose(),
texture = cobblemonResource("textures/gui/summary/type_spacer.png"),
x = (startX + 70.5) / 0.45,
y = (startY + 91.5) / 0.45,
height = 24,
width = 132,
textureHeight = 24,
textureWidth = 132,
scale = 0.45f
)
}
val heldItem = dto.heldItem
if (heldItem != null && !heldItem.isEmpty()) {
context.pose().pushPose()
context.pose().scale(0.95F, 0.95F, 0.95F)
val itemX = (startX + 4) / 0.95
val itemY = (startY + 91) / 0.95
context.renderItem(heldItem, itemX.toInt(), itemY.toInt())
context.pose().popPose()
}
else {
blitk(
matrixStack = context.pose(),
texture = questionMarkIcon,
x = startX + 6,
y = startY + 92.5,
height = 11,
width = 10,
)
}
context.pose().popPose()
}
@JvmOverloads
fun drawCustomPosablePortrait(
identifier: ResourceLocation,
matrixStack: PoseStack,
scale: Float = 13F,
contextScale: Float = 1F,
reversed: Boolean = false,
state: PosableState,
partialTicks: Float,
limbSwing: Float = 0F,
limbSwingAmount: Float = 0F,
ageInTicks: Float = 0F,
headYaw: Float = 0F,
headPitch: Float = 0F,
r: Float = 1F,
g: Float = 1F,
b: Float = 1F,
a: Float = 1F,
doQuirks: Boolean = true
) {
RenderSystem.applyModelViewMatrix()
matrixStack.pushPose()
matrixStack.translate(0.0, PORTRAIT_DIAMETER.toDouble() + 2.0, 0.0)
matrixStack.scale(scale, scale, -scale)
matrixStack.translate(0.0, -PORTRAIT_DIAMETER / 18.0, 0.0)
val sprite = VaryingModelRepository.getSprite(identifier, state, SpriteType.PORTRAIT);
if (sprite == null) {
val model = VaryingModelRepository.getPoser(identifier, state)
state.currentModel = model
val texture = VaryingModelRepository.getTexture(identifier, state)
val context = RenderContext()
model.context = context
VaryingModelRepository.getTextureNoSubstitute(identifier, state).let { context.put(RenderContext.TEXTURE, it) }
context.put(RenderContext.SCALE, contextScale)
context.put(RenderContext.SPECIES, identifier)
context.put(RenderContext.ASPECTS, state.currentAspects)
context.put(RenderContext.POSABLE_STATE, state)
context.put(RenderContext.DO_QUIRKS, doQuirks)
val renderType = RenderType.entityCutout(texture)
val quaternion1 = Axis.YP.rotationDegrees(-32F * if (reversed) -1F else 1F)
val quaternion2 = Axis.XP.rotationDegrees(5F)
val originalPose = state.currentPose
state.setPoseToFirstSuitable(PoseType.PORTRAIT)
state.updatePartialTicks(partialTicks)
model.applyAnimations(null, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch)
originalPose?.let { state.setPose(it) }
matrixStack.translate(
model.portraitTranslation.x * if (reversed) -1F else 1F,
model.portraitTranslation.y + 1.5 * model.portraitScale,
model.portraitTranslation.z - 4
)
matrixStack.scale(model.portraitScale, model.portraitScale, 1 / model.portraitScale)
matrixStack.mulPose(quaternion1)
matrixStack.mulPose(quaternion2)
val light1 = Vector3f(0.2F, 1.0F, -1.0F)
val light2 = Vector3f(0.1F, 0.0F, 8.0F)
RenderSystem.setShaderLights(light1, light2)
quaternion1.conjugate()
val immediate = Minecraft.getInstance().renderBuffers().bufferSource()
val buffer = immediate.getBuffer(renderType)
val packedLight = LightTexture.pack(11, 7)
val colour = toHex(r, g, b, a)
model.withLayerContext(immediate, state, VaryingModelRepository.getLayers(identifier, state)) {
model.render(context, matrixStack, buffer, packedLight, OverlayTexture.NO_OVERLAY, colour)
immediate.endBatch()
}
model.setDefault()
Lighting.setupFor3DItems()
} else {
renderSprite(matrixStack, sprite)
}
matrixStack.popPose()
}
private fun getSpeedRange(form: FormData, level: Int, speedBoost: Double): MutableComponent {
val base = form.baseStats[Stats.SPEED] ?: 0
val maxIV = 31
val maxEV = 252
val minNatureMod = 0.9
val maxNatureMod = 1.1
val minSpeed = getStat(base, level, 0, 0, minNatureMod) * speedBoost
val maxSpeed = getStat(base, level, maxIV, maxEV, maxNatureMod) * speedBoost
return "cobblemon.battle.ui.speed_tier".asTranslated(minSpeed.toInt(), maxSpeed.toInt())
}
private fun getStat(base: Int, level: Int, iv: Int, ev: Int, natureMod: Double): Int {
return floor((floor(((2.0 * base + iv + floor(ev / 4.0)) * level) / 100) + 5) * natureMod).toInt()
}
private fun getMultiplierText(multiplier: Double): MutableComponent {
val rounded = decimalFormat.format(multiplier)
val str = "x$rounded"
return when {
multiplier > 1.0 -> str.green()
multiplier < 1.0 -> str.red()
else -> str.text()
}
}
private fun getFormText(species: Species, form: FormData): MutableComponent {
val speciesName = species.name
val formName = form.name
return if (formName == "Normal") speciesName.text() else "$speciesName-$formName".text()
}
}
}
@@ -9,14 +9,12 @@
package com.cobblemon.mod.common.client.gui.battle.subscreen
import com.cobblemon.mod.common.api.gui.ParentWidget
import com.cobblemon.mod.common.client.battle.SingleActionRequest
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.network.chat.MutableComponent
abstract class BattleActionSelection(
val battleGUI: BattleGUI,
val request: SingleActionRequest,
x: Int,
y: Int,
width: Int,
@@ -11,7 +11,6 @@ package com.cobblemon.mod.common.client.gui.battle.subscreen
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.gui.GuiGraphics
class BattleBackButton(val x: Float, val y: Float) : CobblemonRenderable {
@@ -14,8 +14,10 @@ import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.battle.SingleActionRequest
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.client.gui.battle.widgets.BattleOptionTile
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.narration.NarratableEntry
@@ -27,17 +29,21 @@ import net.minecraft.resources.ResourceLocation
class BattleGeneralActionSelection(
battleGUI: BattleGUI,
request: SingleActionRequest
val request: SingleActionRequest
) : BattleActionSelection(
battleGUI,
request,
BattleGUI.OPTION_ROOT_X,
Minecraft.getInstance().window.guiScaledHeight - BattleGUI.OPTION_VERTICAL_OFFSET,
(BattleOptionTile.OPTION_WIDTH + 3 * BattleGUI.OPTION_HORIZONTAL_SPACING).toInt(),
(BattleOptionTile.OPTION_HEIGHT + 3 * BattleGUI.OPTION_VERTICAL_SPACING).toInt(),
(BattleOptionTile.OPTION_WIDTH + 3 * BattleGUI.OPTION_HORIZONTAL_SPACING),
(BattleOptionTile.OPTION_HEIGHT + 3 * BattleGUI.OPTION_VERTICAL_SPACING),
battleLang("choose_action")
) {
val backButton = BattleBackButton(BattleGUI.OPTION_ROOT_X - 3F, Minecraft.getInstance().window.guiScaledHeight - 22F)
companion object {
val battleInfoSelection = cobblemonResource("textures/gui/battle/team_info.png")
}
val backButton =
BattleBackButton(BattleGUI.OPTION_ROOT_X - 3F, Minecraft.getInstance().window.guiScaledHeight - 22F)
val lastAnwseredRequest = CobblemonClient.battle?.getLastAnsweredRequest()
val tiles = mutableListOf<BattleOptionTile>()
@@ -74,6 +80,12 @@ class BattleGeneralActionSelection(
battleGUI.changeActionSelection(ForfeitConfirmationSelection(battleGUI, request))
playDownSound(Minecraft.getInstance().soundManager)
}
addOption(rank++, "cobblemon.battle.ui.team_info.button".asTranslated(), battleInfoSelection) {
BattleTeamInfoSelection.visible = true
battleGUI.changeActionSelection(BattleTeamInfoSelection(battleGUI))
playDownSound(Minecraft.getInstance().soundManager)
}
}
}
}
@@ -10,34 +10,47 @@ package com.cobblemon.mod.common.client.gui.battle.subscreen
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.moves.MoveTemplate
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.categories.DamageCategories
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.gold
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.battle.Effectiveness
import com.cobblemon.mod.common.client.battle.MoveEffectivenessCalculator
import com.cobblemon.mod.common.client.battle.SingleActionRequest
import com.cobblemon.mod.common.client.gui.MoveCategoryIcon
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.gui.summary.widgets.screens.moves.MovesWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.drawScaledTextJustifiedRight
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.toRGB
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.client.sounds.SoundManager
import net.minecraft.locale.Language
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth.floor
import java.math.RoundingMode
import java.text.DecimalFormat
class BattleMoveSelection(
battleGUI: BattleGUI,
request: SingleActionRequest,
val request: SingleActionRequest,
) : BattleActionSelection(
battleGUI = battleGUI,
request = request,
x = 20,
y = Minecraft.getInstance().window.guiScaledHeight - 84,
width = 100,
@@ -51,7 +64,21 @@ class BattleMoveSelection(
const val MOVE_HORIZONTAL_SPACING = 13F
val moveTexture = cobblemonResource("textures/gui/battle/battle_move.png")
val moveOverlayTexture = cobblemonResource("textures/gui/battle/battle_move_overlay.png")
val moveOverlayTexture = cobblemonResource("textures/gui/battle/move_outline.png")
val moveTooltipIcon = cobblemonResource("textures/gui/battle/move_tooltip_icon.png")
val moveTooltip = cobblemonResource("textures/gui/battle/move_tooltip.png")
val moveTooltipForStatus = cobblemonResource("textures/gui/battle/move_tooltip_status.png")
val effectiveIcon = cobblemonResource("textures/gui/battle/effective_icon.png")
val extremelyEffectiveIcon = cobblemonResource("textures/gui/battle/extremely_effective_icon.png")
val immuneIcon = cobblemonResource("textures/gui/battle/immune_icon.png")
val mostlyIneffectiveIcon = cobblemonResource("textures/gui/battle/mostly_ineffective_icon.png")
val superEffectiveIcon = cobblemonResource("textures/gui/battle/super_effective_icon.png")
val notVeryEffectiveIcon = cobblemonResource("textures/gui/battle/not_very_effective_icon.png")
private val decimalFormat = DecimalFormat("#.##").also {
it.roundingMode = RoundingMode.CEILING
}
}
val moveSet = request.moveSet!!
@@ -74,7 +101,7 @@ class BattleMoveSelection(
BattleGimmickButton.create(gimmick, this, backButton.x + xOff, backButton.y)
}
val shiftButton = BattleShiftButton(x + 22.5F, Minecraft.getInstance().window.guiScaledHeight - 22F )
val shiftButton = BattleShiftButton(x + 22.5F, Minecraft.getInstance().window.guiScaledHeight - 22F)
open class MoveTile(
val moveSelection: BattleMoveSelection,
@@ -138,7 +165,7 @@ class BattleMoveSelection(
// Move Category
MoveCategoryIcon(
x = x + 48,
x = x + 41,
y = y + 14.5,
category = moveTemplate.damageCategory,
opacity = moveSelection.opacity
@@ -168,14 +195,265 @@ class BattleMoveSelection(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = movePPText,
x = x + 75,
x = x + 67,
y = y + 14,
opacity = moveSelection.opacity,
centered = true
)
blitk(
matrixStack = context.pose(),
texture = moveTooltipIcon,
x = x + 82,
y = y + 14,
width = 9,
height = 9,
textureHeight = 18,
textureWidth = 9,
alpha = moveSelection.opacity,
vOffset = if (isTooltipHovered(mouseX.toDouble(), mouseY.toDouble())) 9 else 0
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = "i".text().bold(),
x = x + 87.375,
y = y + 14,
opacity = moveSelection.opacity,
centered = true
)
if (isTooltipHovered(mouseX.toDouble(), mouseY.toDouble())) {
renderTooltip(context)
}
}
fun isHovered(mouseX: Double, mouseY: Double) = mouseX >= x && mouseX <= x + MOVE_WIDTH && mouseY >= y && mouseY <= y + MOVE_HEIGHT
fun renderTooltip(context: GuiGraphics) {
val tooltipY = Minecraft.getInstance().window.guiScaledHeight - 84 - 59 - 4
val tooltipX = 20
val move = Moves.getByNameOrDummy(move.id)
val effectiveness = getMoveEffectiveness(move)
blitk(
matrixStack = context.pose(),
texture = if (effectiveness == null) moveTooltipForStatus else moveTooltip,
x = tooltipX,
y = tooltipY,
width = 195,
height = 56,
alpha = moveSelection.opacity
)
drawScaledText(
context = context,
text = lang("ui.power"),
x = tooltipX + 18,
y = tooltipY + 11,
opacity = moveSelection.opacity,
centered = false,
scale = 0.65f
)
blitk(
matrixStack = context.pose(),
texture = MovesWidget.movesPowerIconResource,
x = (tooltipX + 8.5) / 0.66f,
y = (tooltipY + 9.5) / 0.66f,
width = 10,
height = 10,
alpha = moveSelection.opacity,
scale = 0.66f
)
val powerStr = if (move.power <= 0) "—" else move.power.toInt().toString()
drawScaledTextJustifiedRight(
context = context,
text = powerStr.text(),
x = tooltipX + 73,
y = tooltipY + 11,
opacity = moveSelection.opacity,
scale = 0.65f
)
blitk(
matrixStack = context.pose(),
texture = MovesWidget.movesAccuracyIconResource,
x = (tooltipX + 8.5) / 0.66f,
y = (tooltipY + 26) / 0.66f,
width = 10,
height = 10,
alpha = moveSelection.opacity,
scale = 0.66f
)
drawScaledText(
context = context,
text = lang("ui.accuracy"),
x = tooltipX + 18,
y = tooltipY + 27,
opacity = moveSelection.opacity,
centered = false,
scale = 0.65f
)
val accuracyStr = format(move.accuracy).text()
drawScaledTextJustifiedRight(
context = context,
text = accuracyStr,
x = tooltipX + 73,
y = tooltipY + 27,
opacity = moveSelection.opacity,
scale = 0.65f
)
blitk(
matrixStack = context.pose(),
texture = MovesWidget.movesEffectIconResource,
x = (tooltipX + 8.5) / 0.66f,
y = (tooltipY + 42.5) / 0.66f,
width = 10,
height = 10,
alpha = moveSelection.opacity,
scale = 0.66f
)
drawScaledText(
context = context,
text = lang("ui.effect"),
x = tooltipX + 18,
y = tooltipY + 43.5,
opacity = moveSelection.opacity,
centered = false,
scale = 0.65f
)
val moveEffect = format(move.effectChances.firstOrNull() ?: 0.0).text()
drawScaledTextJustifiedRight(
context = context,
text = moveEffect,
x = tooltipX + 73,
y = tooltipY + 43.5,
opacity = moveSelection.opacity,
scale = 0.65f
)
val moveDescription = move.description
val lines = Minecraft.getInstance().font.splitter.splitLines(moveDescription, 200, moveDescription.style)
val orderedLines = Language.getInstance().getVisualOrder(lines)
val maxLines = if (effectiveness == null) 6 else 4
orderedLines.take(maxLines).forEachIndexed { index, line ->
drawScaledText(
context = context,
text = line,
x = tooltipX + 85,
y = tooltipY + 11 + (6 * index),
opacity = moveSelection.opacity,
scaleX = 0.5f,
scaleY = 0.5f,
centered = false
)
}
if (effectiveness == null) return
val effectivenessText = getEffectivenessText(effectiveness)
val effectivenessIcon = getEffectivenessIcon(effectiveness)
val effectivenessValue = getEffectivenessValue(effectiveness)
blitk(
matrixStack = context.pose(),
texture = effectivenessIcon,
x = (tooltipX + 84) / 0.5f,
y = (tooltipY + 43) / 0.5f,
width = 12,
height = 12,
alpha = moveSelection.opacity,
scale = 0.5f
)
drawScaledText(
context = context,
text = effectivenessText,
x = tooltipX + 94,
y = tooltipY + 43.5,
opacity = moveSelection.opacity,
centered = false,
scale = 0.65f
)
drawScaledText(
context = context,
text = effectivenessValue,
x = tooltipX + 168,
y = tooltipY + 43.5,
opacity = moveSelection.opacity,
centered = false,
scale = 0.65f
)
}
private fun getEffectivenessValue(effectiveness: Effectiveness): MutableComponent {
return when (effectiveness) {
Effectiveness.IMMUNE -> "0.00x".text()
Effectiveness.MOSTLY_INEFFECTIVE -> "0.25x".text()
Effectiveness.NOT_VERY_EFFECTIVE -> "0.50x".text()
Effectiveness.EFFECTIVE -> "1.00x".text()
Effectiveness.SUPER_EFFECTIVE -> "2.00x".text()
Effectiveness.EXTREMELY_EFFECTIVE -> "4.00x".text()
}
}
private fun getEffectivenessIcon(effectiveness: Effectiveness): ResourceLocation {
return when (effectiveness) {
Effectiveness.IMMUNE -> immuneIcon
Effectiveness.MOSTLY_INEFFECTIVE -> mostlyIneffectiveIcon
Effectiveness.NOT_VERY_EFFECTIVE -> notVeryEffectiveIcon
Effectiveness.EFFECTIVE -> effectiveIcon
Effectiveness.SUPER_EFFECTIVE -> superEffectiveIcon
Effectiveness.EXTREMELY_EFFECTIVE -> extremelyEffectiveIcon
}
}
private fun getEffectivenessText(effectiveness: Effectiveness): MutableComponent {
return when (effectiveness) {
Effectiveness.IMMUNE -> "cobblemon.battle.ui.effectiveness.has_no_effect".asTranslated()
Effectiveness.MOSTLY_INEFFECTIVE -> "cobblemon.battle.ui.effectiveness.mostly_ineffective".asTranslated()
Effectiveness.NOT_VERY_EFFECTIVE -> "cobblemon.battle.ui.effectiveness.not_very_effective".asTranslated()
Effectiveness.EFFECTIVE -> "cobblemon.battle.ui.effectiveness.effective".asTranslated()
Effectiveness.SUPER_EFFECTIVE -> "cobblemon.battle.ui.effectiveness.super_effective".asTranslated()
Effectiveness.EXTREMELY_EFFECTIVE -> "cobblemon.battle.ui.effectiveness.extremely_effective".asTranslated()
}
}
private fun getMoveEffectiveness(move: MoveTemplate): Effectiveness? {
val battle = CobblemonClient.battle ?: return null
val opponents = battle.side2.activeClientBattlePokemon.toList()
if (opponents.size != 1) return null
val opponent = battle.side2.activeClientBattlePokemon.first().battlePokemon ?: return null
val aspects: Set<String> = opponent.state.currentAspects
val opponentForm = opponent.species.getForm(aspects)
if (move.damageCategory == DamageCategories.STATUS) return null
return MoveEffectivenessCalculator.getMoveEffectiveness(move.elementalType, opponentForm.primaryType, opponentForm.secondaryType)
}
fun format(input: Double): String {
if (input <= 0) return "—"
return "${decimalFormat.format(input)}%"
}
fun isHovered(mouseX: Double, mouseY: Double): Boolean {
return !isTooltipHovered(mouseX, mouseY) && mouseX >= x && mouseX <= x + MOVE_WIDTH && mouseY >= y && mouseY <= y + MOVE_HEIGHT
}
fun isTooltipHovered(mouseX: Double, mouseY: Double): Boolean {
return mouseX >= x + 82 && mouseX <= x + MOVE_WIDTH && mouseY >= y + 14 && mouseY <= y + MOVE_HEIGHT
}
fun onClick() {
if (!selectable) return
@@ -204,7 +482,15 @@ class BattleMoveSelection(
if(request.activePokemon.getFormat().battleType.pokemonPerSide == 1) {
move.onClick()
} else {
battleGUI.changeActionSelection(BattleTargetSelection(battleGUI, request, move.move, move.response.gimmickID, move.move.gimmickMove))
battleGUI.changeActionSelection(
BattleTargetSelection(
battleGUI,
request,
move.move,
move.response.gimmickID,
move.move.gimmickMove
)
)
playDownSound(Minecraft.getInstance().soundManager)
}
return true
@@ -11,7 +11,6 @@ package com.cobblemon.mod.common.client.gui.battle.subscreen
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.resources.ResourceLocation
class BattleShiftButton(val x: Float, val y: Float) {
companion object {
@@ -19,7 +18,6 @@ class BattleShiftButton(val x: Float, val y: Float) {
const val HEIGHT = 34
const val SCALE = 0.5F
val baseTexture = cobblemonResource("textures/gui/battle/triple_battle_shift.png")
}
fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
@@ -44,13 +44,12 @@ import org.joml.Vector3f
class BattleSwitchPokemonSelection(
battleGUI: BattleGUI,
request: SingleActionRequest
val request: SingleActionRequest
) : BattleActionSelection(
battleGUI,
request,
x = 0,
y = if (Minecraft.getInstance().window.guiScaledHeight > 304) (Minecraft.getInstance().window.guiScaledHeight / 2) - (BACKGROUND_HEIGHT / 2)
else Minecraft.getInstance().window.guiScaledHeight - (BACKGROUND_HEIGHT + 78),
else Minecraft.getInstance().window.guiScaledHeight - (BACKGROUND_HEIGHT + 78),
width = Minecraft.getInstance().window.guiScaledWidth,
height = Minecraft.getInstance().window.guiScaledHeight,
battleLang("switch_pokemon")
@@ -64,7 +63,7 @@ class BattleSwitchPokemonSelection(
}
val tiles = mutableListOf<SwitchTile>()
val backButton = BattleBackButton(x + 9F, Minecraft.getInstance().window.guiScaledHeight - 22F )
val backButton = BattleBackButton(x + 9F, Minecraft.getInstance().window.guiScaledHeight - 22F)
var isReviving = false
@@ -78,9 +77,9 @@ class BattleSwitchPokemonSelection(
?.let { showdownPokemon to it }
}.filter { it.second.uuid !in switchingInPokemon }
isReviving = request.side.pokemon.any { it.reviving && it.uuid == request.activePokemon.battlePokemon?.uuid }
isReviving = request.side!!.pokemon.any { it.reviving && it.uuid == request.activePokemon.battlePokemon?.uuid }
if (request.forceSwitch && !isReviving && showdownPokemonToPokemon.all {
(it.second.uuid in battleGUI.actor!!.activePokemon.map { it.battlePokemon?.uuid } || ("fnt" in it.first.condition))}) { // on field or fainted
(it.second.uuid in battleGUI.actor!!.activePokemon.map { it.battlePokemon?.uuid } || ("fnt" in it.first.condition))}) { // on field or fainted
// Occurs after a multi-knock out and the player doesn't have enough pokemon to fill every vacant slot
battleGUI.selectAction(request, PassActionResponse)
}
@@ -198,7 +197,7 @@ class BattleSwitchPokemonSelection(
val healthRatioSplits = showdownPokemon.condition.split(" ")[0].split("/")
try {
val (hp, maxHp) = if (healthRatioSplits.size == 1) 0 to 0
else healthRatioSplits[0].toInt() to pokemon.maxHealth
else healthRatioSplits[0].toInt() to pokemon.maxHealth
val hpRatio = hp / maxHp.toFloat()
val status = pokemon.status?.status?.showdownName
@@ -266,7 +265,7 @@ class BattleSwitchPokemonSelection(
matrixStack.pushPose()
matrixStack.translate(0.0, 0.0, 100.0)
// Held Item
val heldItem = pokemon.heldItemNoCopy()
val heldItem = pokemon.heldItem
if (!heldItem.isEmpty) {
renderScaledGuiItemIcon(
matrixStack = matrixStack,
@@ -24,7 +24,6 @@ import com.cobblemon.mod.common.client.battle.ActiveClientBattlePokemon
import com.cobblemon.mod.common.client.battle.SingleActionRequest
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleSwitchPokemonSelection.Companion
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
@@ -44,13 +43,12 @@ import kotlin.math.sin
class BattleTargetSelection(
battleGUI: BattleGUI,
request: SingleActionRequest,
val request: SingleActionRequest,
val move: InBattleMove,
val gimmickID: String?,
gimmickMove: InBattleGimmickMove?
) : BattleActionSelection(
battleGUI = battleGUI,
request = request,
x = 0,
y = if (Minecraft.getInstance().window.guiScaledHeight > 304) (Minecraft.getInstance().window.guiScaledHeight / 2) - (BattleSwitchPokemonSelection.BACKGROUND_HEIGHT / 2)
else Minecraft.getInstance().window.guiScaledHeight - (BattleSwitchPokemonSelection.BACKGROUND_HEIGHT + 78),
@@ -0,0 +1,863 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.battle.subscreen
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.gui.renderSprite
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.font
import com.cobblemon.mod.common.api.text.green
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.battle.ClientBattleActor
import com.cobblemon.mod.common.client.battle.ClientBattleInformationRepository
import com.cobblemon.mod.common.client.battle.ClientBattlePokemon
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.drawScaledTextJustifiedRight
import com.cobblemon.mod.common.client.render.getDepletableRedGreen
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.renderScaledGuiItemIcon
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay.Companion.PORTRAIT_DIAMETER
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay.Companion.questionMarkIcon
import com.cobblemon.mod.common.client.gui.battle.preview.TeamPreviewWidget
import com.cobblemon.mod.common.client.gui.battle.preview.TeamPreviewWidget.PokemonTile
import com.cobblemon.mod.common.client.render.SpriteType
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.net.messages.client.battle.BattlePokemonDTO
import com.cobblemon.mod.common.util.toHex
import com.mojang.blaze3d.platform.Lighting
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.math.Axis
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.narration.NarratableEntry
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.client.sounds.SoundManager
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import org.joml.Quaternionf
import org.joml.Vector3f
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.collections.get
import kotlin.math.ceil
import kotlin.math.floor
class BattleTeamInfoSelection(
battleGUI: BattleGUI
) : BattleActionSelection(
battleGUI,
x = 0,
y = if (Minecraft.getInstance().window.guiScaledHeight > 304) (Minecraft.getInstance().window.guiScaledHeight / 2) - (BACKGROUND_HEIGHT / 2)
else Minecraft.getInstance().window.guiScaledHeight - (BACKGROUND_HEIGHT + 78),
width = Minecraft.getInstance().window.guiScaledWidth,
height = Minecraft.getInstance().window.guiScaledHeight,
"cobblemon.battle.ui.team_info".asTranslated()
) {
companion object {
const val SLOT_HORIZONTAL_SPACING = 4F
const val SLOT_VERTICAL_SPACING = 2F
const val BACKGROUND_HEIGHT = 148
val underlayTexture = cobblemonResource("textures/gui/battle/selection_underlay.png")
val title = cobblemonResource("textures/gui/battle/turn_counter.png")
var visible = false
}
val teamTiles = mutableListOf<PokemonTile>()
val opponentTiles = mutableListOf<PokemonTile>()
val backButton = BattleBackButton(x + 9F, Minecraft.getInstance().window.guiScaledHeight - 22F)
init {
addTiles()
}
fun addTiles() {
val battle = CobblemonClient.battle ?: return
val actors = mutableListOf<ClientBattleActor>()
battle.sides.forEach { side ->
side.actors.forEach { actors.add(it) }
}
if (actors.size != 2) return
val displayOrder = if (actors.last().uuid == Minecraft.getInstance().player?.uuid) actors.reversed() else actors
val actorToPokemon = displayOrder.map { it to ClientBattleInformationRepository.actors[it.uuid] }
actorToPokemon.first().second?.forEachIndexed { index, pokemon ->
if (pokemon == null) return@forEachIndexed
val (slotX, slotY) = getSlotPosition(index, 1)
teamTiles.add(PokemonTile(slotX, slotY, pokemon, this, false))
}
actorToPokemon.last().second?.forEachIndexed { index, pokemon ->
if (pokemon == null) return@forEachIndexed
val (slotX, slotY) = getSlotPosition(index, 2)
opponentTiles.add(PokemonTile(slotX, slotY, pokemon, this, true))
}
}
fun getSlotPosition(index: Int, side: Int): Pair<Float, Float> {
val startX = if (side == 1) 10 else width - 10 - (41 + TeamPreviewWidget.Companion.SLOT_HORIZONTAL_SPACING) * 2
val startY = y + 15
val row = index / 2
val column = index % 2
val slotX = startX.toFloat() + column * (SLOT_HORIZONTAL_SPACING + 41)
val slotY = startY.toFloat() + row * (SLOT_VERTICAL_SPACING + 39)
return Pair(slotX, slotY)
}
private fun getHoveredTile(team: Boolean, mouseX: Int, mouseY: Int): Int {
val tiles = if (team) teamTiles else opponentTiles
return tiles.indexOfFirst { it.isHovered(mouseX.toDouble(), mouseY.toDouble()) }
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
if (opacity <= 0.05F) return
val matrixStack = context.pose()
val mc = Minecraft.getInstance()
blitk(
matrixStack = matrixStack,
texture = underlayTexture,
x = x,
y = y,
width = width,
height = BACKGROUND_HEIGHT
)
blitk(
matrixStack = matrixStack,
texture = title,
x = (mc.window.guiScaledWidth / 2) - (103 / 2),
y = 4,
width = 103,
height = 22
)
drawScaledText(
context = context,
text = "cobblemon.battle.ui.team_info.title".text().bold(),
x = (mc.window.guiScaledWidth / 2),
y = 6,
scale = 0.75f,
shadow = true,
centered = true
)
teamTiles.forEachIndexed { index, tile ->
val hoveredTile = getHoveredTile(true, mouseX, mouseY)
if (shouldHide(hoveredTile, index, false)) return@forEachIndexed
tile.render(context, mouseX.toDouble(), mouseY.toDouble(), delta)
}
opponentTiles.forEachIndexed { index, tile ->
val hoveredTile = getHoveredTile(false, mouseX, mouseY)
if (shouldHide(hoveredTile, index, true)) return@forEachIndexed
tile.render(context, mouseX.toDouble(), mouseY.toDouble(), delta)
}
backButton.render(context, mouseX, mouseY, delta)
}
private fun shouldHide(hoveredTile: Int, currentTile: Int, reversed: Boolean): Boolean {
if (hoveredTile != -1) {
val hoveredRow = hoveredTile / 2
val hoveredColumn = hoveredTile % 2
val currentRow = currentTile / 2
val currentColumn = currentTile % 2
if (!reversed && hoveredColumn < currentColumn && hoveredRow <= currentRow) return true
if (reversed && hoveredColumn > currentColumn && hoveredRow <= currentRow) return true
}
return false
}
override fun mousePrimaryClicked(mouseX: Double, mouseY: Double): Boolean {
if (backButton.isHovered(mouseX, mouseY)) {
battleGUI.changeActionSelection(null)
playDownSound(Minecraft.getInstance().soundManager)
Companion.visible = false
return true
}
return false
}
override fun defaultButtonNarrationText(builder: NarrationElementOutput) {
}
override fun playDownSound(soundManager: SoundManager) {
soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
}
override fun narrationPriority() = NarratableEntry.NarrationPriority.HOVERED
class PokemonTile(
val x: Float,
val y: Float,
val pokemonDto: BattlePokemonDTO,
val widget: BattleTeamInfoSelection,
val reversed: Boolean
) {
companion object {
const val SELECT_WIDTH = 41
const val SELECT_HEIGHT = 39
const val SCALE = 0.5F
val expandedPokemonInfo = cobblemonResource("textures/gui/battle/expanded_pokemon_info_center.png")
val pokemonTile = cobblemonResource("textures/gui/battle/pokemon_tile.png")
val pokemonTileReversed = cobblemonResource("textures/gui/battle/pokemon_tile_reversed.png")
val pokemonTileDisabled = cobblemonResource("textures/gui/battle/pokemon_tile_disabled.png")
val pokemonTileDisabledReversed = cobblemonResource("textures/gui/battle/pokemon_tile_disabled_reversed.png")
val pokemonTileSelected = cobblemonResource("textures/gui/battle/pokemon_tile_selected.png")
val pokemonTileSelectedReversed = cobblemonResource("textures/gui/battle/pokemon_tile_selected_reversed.png")
val hoverGapSelected = cobblemonResource("textures/gui/battle/hover_gap_selected.png")
val hoverGap = cobblemonResource("textures/gui/battle/hover_gap.png")
val hoverGapReversed = cobblemonResource("textures/gui/battle/hover_gap_reversed.png")
private val decimalFormat = DecimalFormat("0.00").also {
it.roundingMode = RoundingMode.CEILING
}
}
val state = FloatingState()
fun isHovered(mouseX: Double, mouseY: Double) = mouseX in x..(x + SELECT_WIDTH) && mouseY in (y..(y + SELECT_HEIGHT))
fun render(context: GuiGraphics, mouseX: Double, mouseY: Double, deltaTicks: Float) {
val pokemon = pokemonDto.activeBattlePokemonDTO ?: return
val battlePokemon = ClientBattlePokemon(
uuid = pokemon.uuid,
properties = pokemon.properties,
aspects = pokemon.aspects,
displayName = pokemon.displayName,
hpValue = pokemon.hpValue,
maxHp = pokemon.maxHp,
isHpFlat = pokemon.isFlatHp,
status = pokemon.status,
statChanges = pokemon.statChanges,
)
state.currentAspects = pokemon.aspects
val matrixStack = context.pose()
val isFainted = pokemonDto.fainted
blitk(
matrixStack = matrixStack,
texture = cobblemonResource("textures/gui/battle/battle_info_underlay.png"),
x = x + 5 + (if (reversed) 3 else 0),
y = y + 4,
width = 28,
height = 28
)
val speciesToDisplay = battlePokemon.species
val stateToDisplay = battlePokemon.state
context.enableScissor(
(x + 5 + (if (reversed) 3 else 0)).toInt(),
(y + 5).toInt(),
(x + 5 + 28 + (if (reversed) 3 else 0)).toInt(),
(y + 5 + 28).toInt(),
)
matrixStack.pushPose()
matrixStack.translate(
x + (if (reversed) 3 else 0) + 5 + 28 / 2.0,
y.toDouble() + 5 - 5.0,
0.0
)
drawCustomPosablePortrait(
identifier = speciesToDisplay.resourceIdentifier,
matrixStack = matrixStack,
scale = 18f,
contextScale = speciesToDisplay.getForm(stateToDisplay.currentAspects).baseScale,
reversed = reversed,
doQuirks = false,
state = stateToDisplay,
partialTicks = if (Cobblemon.config.animateBattleTiles) deltaTicks else 0F
)
matrixStack.popPose()
context.disableScissor()
val tile = if (reversed) pokemonTileReversed else pokemonTile
val disabled = if (reversed) pokemonTileDisabledReversed else pokemonTileDisabled
val selected = if (reversed) pokemonTileSelectedReversed else pokemonTileSelected
if (isFainted) {
blitk(
matrixStack = matrixStack,
texture = disabled,
x = x,
y = y,
width = 41,
height = 39
)
}
else {
blitk(
matrixStack = matrixStack,
texture = tile,
x = x,
y = y,
width = 41,
height = 39
)
}
if (isHovered(mouseX, mouseY)) {
renderExpandedInfo(context, reversed, battlePokemon, pokemonDto)
}
}
private fun renderExpandedInfo(context: GuiGraphics, reversed: Boolean, pokemon: ClientBattlePokemon, dto: BattlePokemonDTO) {
context.pose().pushPose()
context.pose().translate(0.0, 0.0, 300.0)
val startX = if (reversed) x - 136 else x + 40
val startY = y
val species = pokemon.species
val form = species.getForm(pokemon.state.currentAspects)
val expandedInfoTexture = expandedPokemonInfo
val gap = if (reversed) hoverGapReversed else hoverGap
val xOffset = if (reversed) 136 else -7
blitk(
matrixStack = context.pose(),
texture = gap,
x = startX + xOffset,
y = startY,
height = 36,
width = 8,
textureHeight = 36,
textureWidth = 8,
)
blitk(
matrixStack = context.pose(),
texture = expandedInfoTexture,
x = startX,
y = startY,
height = 112,
width = 137,
textureHeight = 112,
textureWidth = 137,
)
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.pokemon_form".asTranslated().bold(),
x = startX + 36,
y = startY + 6.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = getFormText(species, form),
x = startX + 36,
y = startY + 13.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.buffs".asTranslated().bold(),
x = startX + 36,
y = startY + 23.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = "cobblemon.stat.attack.name".asTranslated(),
x = startX + 10,
y = startY + 31.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.ATTACK)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 31.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.defence.name".asTranslated(),
x = startX + 10,
y = startY + 39.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.DEFENCE)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 39.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.battle.ui.stats.special_attack".asTranslated(),
x = startX + 10,
y = startY + 47.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.SPECIAL_ATTACK)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 47.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.battle.ui.stats.special_defence".asTranslated(),
x = startX + 10,
y = startY + 55.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.SPECIAL_DEFENCE)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 55.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.speed.name".asTranslated(),
x = startX + 10,
y = startY + 63.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.SPEED)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 63.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.accuracy.name".asTranslated(),
x = startX + 10,
y = startY + 71.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto.buffs.get(Stats.ACCURACY)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 71.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.evasion.name".asTranslated(),
x = startX + 10,
y = startY + 79.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
dto?.buffs?.get(Stats.EVASION)?.let {
drawScaledText(
context = context,
text = getMultiplierText(it),
x = startX + 50,
y = startY + 79.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = false
)
}
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.ability".asTranslated().bold(),
x = startX + 100,
y = startY + 6.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
val ability = dto.ability?.let { Abilities.get(it) }
val abilityName = ability?.displayName?.text() ?: "?".text()
drawScaledText(
context = context,
text = abilityName,
x = startX + 100,
y = startY + 13.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.moves".asTranslated().bold(),
x = startX + 100,
y = startY + 23.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
if (dto.moves.size != 4) {
for (i in 0 until 4) {
drawScaledText(
context = context,
text = "?".text(),
x = startX + 100,
y = startY + 31.5 + i * 8,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
}
}
else {
val questionMarkText = "?".text()
dto.moves.forEachIndexed { index, moveDTO ->
val move = moveDTO?.move ?: questionMarkText
drawScaledText(
context = context,
text = move.string.text(),
x = if (move == questionMarkText) startX + 100 else startX + 74,
y = startY + 31.5 + index * 8,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = move == questionMarkText
)
if (moveDTO != null) {
drawScaledTextJustifiedRight(
context = context,
text = moveDTO.timesUsed.toString().text(),
x = startX + 127.5,
y = startY + 31.5 + index * 8,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
)
}
}
}
if (dto.speed != null) {
val speed = dto.speed
val speedBoost = dto.buffs.get(Stats.SPEED) ?: 1.0
val speedAfterBoost = (speed * speedBoost).toInt()
drawScaledText(
context = context,
text = "cobblemon.stat.speed.name".asTranslated(),
x = startX + 100,
y = startY + 65.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = speedAfterBoost.toString().text(),
x = startX + 100,
y = startY + 72.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
}
else {
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.speed_tier".asTranslated(),
x = startX + 100,
y = startY + 65.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
val speedBoost = dto.buffs.get(Stats.SPEED) ?: 1.0
val speedTier = getSpeedRange(form, pokemon.level, speedBoost)
drawScaledText(
context = context,
text = speedTier,
x = startX + 100,
y = startY + 72.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "cobblemon.battle.ui.label.held_item".asTranslated(),
x = startX + 40,
y = startY + 99.5,
scale = BattleOverlay.Companion.SCALE,
shadow = true,
centered = true
)
TypeIcon(
x = startX + 100.5,
y = startY + 88,
type = form.primaryType,
secondaryType = form.secondaryType,
small = false,
centeredX = true
).render(context)
if (form.secondaryType != null) {
blitk(
matrixStack = context.pose(),
texture = cobblemonResource("textures/gui/battle/type_spacer_double.png"),
x = (startX + 69.0) / 0.45,
y = (startY + 91.5) / 0.45,
height = 24,
width = 140,
textureHeight = 24,
textureWidth = 140,
scale = 0.45f
)
}
else {
blitk(
matrixStack = context.pose(),
texture = cobblemonResource("textures/gui/summary/type_spacer.png"),
x = (startX + 70.5) / 0.45,
y = (startY + 91.5) / 0.45,
height = 24,
width = 132,
textureHeight = 24,
textureWidth = 132,
scale = 0.45f
)
}
val heldItem = dto.heldItem
if (heldItem != null && !heldItem.isEmpty()) {
context.pose().pushPose()
context.pose().scale(0.95F, 0.95F, 0.95F)
val itemX = (startX + 4) / 0.95
val itemY = (startY + 91) / 0.95
context.renderItem(heldItem, itemX.toInt(), itemY.toInt())
context.pose().popPose()
}
else {
blitk(
matrixStack = context.pose(),
texture = questionMarkIcon,
x = startX + 6,
y = startY + 92.5,
height = 11,
width = 10,
)
}
context.pose().popPose()
}
@JvmOverloads
fun drawCustomPosablePortrait(
identifier: ResourceLocation,
matrixStack: PoseStack,
scale: Float = 13F,
contextScale: Float = 1F,
reversed: Boolean = false,
state: PosableState,
partialTicks: Float,
limbSwing: Float = 0F,
limbSwingAmount: Float = 0F,
ageInTicks: Float = 0F,
headYaw: Float = 0F,
headPitch: Float = 0F,
r: Float = 1F,
g: Float = 1F,
b: Float = 1F,
a: Float = 1F,
doQuirks: Boolean = true
) {
RenderSystem.applyModelViewMatrix()
matrixStack.pushPose()
matrixStack.translate(0.0, PORTRAIT_DIAMETER.toDouble() + 2.0, 0.0)
matrixStack.scale(scale, scale, -scale)
matrixStack.translate(0.0, -PORTRAIT_DIAMETER / 18.0, 0.0)
val sprite = VaryingModelRepository.getSprite(identifier, state, SpriteType.PORTRAIT);
if (sprite == null) {
val model = VaryingModelRepository.getPoser(identifier, state)
state.currentModel = model
val texture = VaryingModelRepository.getTexture(identifier, state)
val context = RenderContext()
model.context = context
VaryingModelRepository.getTextureNoSubstitute(identifier, state).let { context.put(RenderContext.TEXTURE, it) }
context.put(RenderContext.SCALE, contextScale)
context.put(RenderContext.SPECIES, identifier)
context.put(RenderContext.ASPECTS, state.currentAspects)
context.put(RenderContext.POSABLE_STATE, state)
context.put(RenderContext.DO_QUIRKS, doQuirks)
val renderType = RenderType.entityCutout(texture)
val quaternion1 = Axis.YP.rotationDegrees(-32F * if (reversed) -1F else 1F)
val quaternion2 = Axis.XP.rotationDegrees(5F)
val originalPose = state.currentPose
state.setPoseToFirstSuitable(PoseType.PORTRAIT)
state.updatePartialTicks(partialTicks)
model.applyAnimations(null, state, limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch)
originalPose?.let { state.setPose(it) }
matrixStack.translate(
model.portraitTranslation.x * if (reversed) -1F else 1F,
model.portraitTranslation.y + 1.5 * model.portraitScale,
model.portraitTranslation.z - 4
)
matrixStack.scale(model.portraitScale, model.portraitScale, 1 / model.portraitScale)
matrixStack.mulPose(quaternion1)
matrixStack.mulPose(quaternion2)
val light1 = Vector3f(0.2F, 1.0F, -1.0F)
val light2 = Vector3f(0.1F, 0.0F, 8.0F)
RenderSystem.setShaderLights(light1, light2)
quaternion1.conjugate()
val immediate = Minecraft.getInstance().renderBuffers().bufferSource()
val buffer = immediate.getBuffer(renderType)
val packedLight = LightTexture.pack(11, 7)
val colour = toHex(r, g, b, a)
model.withLayerContext(immediate, state, VaryingModelRepository.getLayers(identifier, state)) {
model.render(context, matrixStack, buffer, packedLight, OverlayTexture.NO_OVERLAY, colour)
immediate.endBatch()
}
model.setDefault()
Lighting.setupFor3DItems()
} else {
renderSprite(matrixStack, sprite)
}
matrixStack.popPose()
}
private fun getSpeedRange(form: FormData, level: Int, speedBoost: Double): MutableComponent {
val base = form.baseStats[Stats.SPEED] ?: 0
val maxIV = 31
val maxEV = 252
val minNatureMod = 0.9
val maxNatureMod = 1.1
val minSpeed = getStat(base, level, 0, 0, minNatureMod) * speedBoost
val maxSpeed = getStat(base, level, maxIV, maxEV, maxNatureMod) * speedBoost
return "cobblemon.battle.ui.speed_tier".asTranslated(minSpeed.toInt(), maxSpeed.toInt())
}
private fun getStat(base: Int, level: Int, iv: Int, ev: Int, natureMod: Double): Int {
return floor((floor(((2.0 * base + iv + floor(ev / 4.0)) * level) / 100) + 5) * natureMod).toInt()
}
private fun getMultiplierText(multiplier: Double): MutableComponent {
val rounded = decimalFormat.format(multiplier)
val str = "x$rounded"
return when {
multiplier > 1.0 -> str.green()
multiplier < 1.0 -> str.red()
else -> str.text()
}
}
private fun getFormText(species: Species, form: FormData): MutableComponent {
val speciesName = species.name
val formName = form.name
return if (formName == "Normal") speciesName.text() else "$speciesName-$formName".text()
}
}
}
@@ -25,10 +25,9 @@ import net.minecraft.client.gui.GuiGraphics
class ForfeitConfirmationSelection(
battleGUI: BattleGUI,
request: SingleActionRequest
val request: SingleActionRequest
) : BattleActionSelection(
battleGUI,
request,
x = 0,
y = 0,
width = WIDTH,
@@ -10,14 +10,21 @@ package com.cobblemon.mod.common.client.gui.battle.widgets
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.battle.ClientBattleMessageQueue
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleTeamInfoSelection
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.ObjectSelectionList
import net.minecraft.locale.Language
import net.minecraft.network.chat.Component
import net.minecraft.util.FormattedCharSequence
import kotlin.math.max
import kotlin.math.min
import kotlin.math.round
/**
* Pane for seeing and interacting with battle messages.
@@ -42,23 +49,38 @@ class BattleMessagePane(
val appropriateY: Int
get() = minecraft.window.guiScaledHeight - (30 + (if (expanded) FRAME_EXPANDED_HEIGHT else FRAME_HEIGHT))
val battleMessages = mutableListOf<Component>()
init {
correctSize()
//setRenderBackground(false)
scrollAmount = maxScroll.toDouble()
messageQueue.subscribe {
val fullyScrolledDown = maxScroll - scrollAmount < 10
addEntry(BattleMessageLine(this, it))
if (fullyScrolledDown) {
scrollAmount = maxScroll.toDouble()
battleMessages.add(it)
correctBattleText()
}
}
private fun correctBattleText() {
val isFullyScrolled = maxScroll - scrollAmount < 10
clearEntries()
val textRenderer = Minecraft.getInstance().font
for (message in battleMessages) {
val line = message.copy().setStyle(message.style.withBold(true).withFont(CobblemonResources.DEFAULT_LARGE))
val wrappedLines = textRenderer.splitter.splitLines(line, battleLogWidth - 27, line.style)
val lines = Language.getInstance().getVisualOrder(wrappedLines)
for (finalLine in lines) {
addEntry(BattleMessageLine(this, finalLine))
}
}
if (isFullyScrolled) {
scrollAmount = maxScroll.toDouble()
}
}
private fun correctSize() {
val textBoxHeight = if (expanded) TEXT_BOX_HEIGHT * 2 else TEXT_BOX_HEIGHT
setRectangle(TEXT_BOX_WIDTH, textBoxHeight, appropriateY + 6, appropriateY + 6)
this.x = appropriateX
setRectangle(battleLogWidth - 11, battleLogHeight - 13, battleLogX.toInt(), battleLogY.toInt() + 8)
}
companion object {
@@ -71,10 +93,52 @@ class BattleMessagePane(
const val TEXT_BOX_HEIGHT = 46
const val EXPAND_TOGGLE_SIZE = 5
private val battleMessagePaneFrameResource = cobblemonResource("textures/gui/battle/battle_log.png")
private val battleMessagePaneFrameExpandedResource = cobblemonResource("textures/gui/battle/battle_log_expanded.png")
private val battleMessageHighlight = cobblemonResource("textures/gui/battle/battle_log_row_selected_color.png")
val topTexture = cobblemonResource("textures/gui/battle/log/top.png")
val bottomTexture = cobblemonResource("textures/gui/battle/log/bottom.png")
val leftTexture = cobblemonResource("textures/gui/battle/log/left.png")
val rightTexture = cobblemonResource("textures/gui/battle/log/right.png")
val topLeftTexture = cobblemonResource("textures/gui/battle/log/top_left.png")
val topRightTexture = cobblemonResource("textures/gui/battle/log/top_right.png")
val bottomLeftTexture = cobblemonResource("textures/gui/battle/log/bottom_left.png")
val bottomRightTexture = cobblemonResource("textures/gui/battle/log/bottom_right.png")
val centerTexture = cobblemonResource("textures/gui/battle/log/center.png")
private var expanded = false
var battleLogWidth: Int = 153
set(value) {
val max = Minecraft.getInstance().window.guiScaledWidth - 20
if (battleLogX + value <= max) {
field = value
}
}
var battleLogHeight: Int = 46
set(value) {
val max = Minecraft.getInstance().window.guiScaledHeight - 20
if (battleLogY + value <= max) {
field = value
}
}
var battleLogX: Double = defaultLogX
get() {
val max = Minecraft.getInstance().window.guiScaledWidth - battleLogWidth
return max(min(field, max.toDouble() - 20), 4.0)
}
var battleLogY: Double = defaultLogY
get() {
val max = Minecraft.getInstance().window.guiScaledHeight - battleLogHeight
return max(min(field, max.toDouble() - 20), 4.0)
}
val defaultLogX: Double
get() = Minecraft.getInstance().window.guiScaledWidth - 181.0
val defaultLogY: Double
get() = Minecraft.getInstance().window.guiScaledHeight - 85.0
}
override fun addEntry(entry: BattleMessageLine): Int {
@@ -82,7 +146,7 @@ class BattleMessagePane(
}
override fun getRowLeft(): Int {
return super.getRowLeft() + 4
return x + 10
}
override fun renderSelection(guiGraphics: GuiGraphics, top: Int, width: Int, height: Int, outerColor: Int, innerColor: Int) {
@@ -102,7 +166,7 @@ class BattleMessagePane(
x = x + 6,
y = top - 2,
height = 1,
width = LINE_WIDTH,
width = battleLogWidth - 18,
alpha = opacity
)
@@ -112,14 +176,14 @@ class BattleMessagePane(
x = x + 6,
y = top + 7,
height = 1,
width = LINE_WIDTH,
width = battleLogWidth - 18,
alpha = opacity
)
blitk(
matrixStack = guiGraphics.pose(),
texture = battleMessageHighlight,
x = x + 6 + LINE_WIDTH - 1,
x = x + 6 + battleLogWidth - 19,
y = top - 2,
height = 10,
width = 1,
@@ -128,48 +192,144 @@ class BattleMessagePane(
}
override fun getRowWidth(): Int {
return LINE_WIDTH
return battleLogWidth - 16
}
override fun getScrollbarPosition(): Int {
return this.x + 154
return this.x + battleLogWidth - 16
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) {
if (BattleTeamInfoSelection.visible) return
correctSize()
val isFullyScrolled = opacity != 1f || maxScroll - scrollAmount < 2
if (isFullyScrolled) { scrollAmount = maxScroll.toDouble() }
blitk(
matrixStack = context.pose(),
texture = if (expanded) battleMessagePaneFrameExpandedResource else battleMessagePaneFrameResource,
x = this.x,
y = appropriateY,
height = if (expanded) FRAME_EXPANDED_HEIGHT else FRAME_HEIGHT,
width = FRAME_WIDTH,
texture = topLeftTexture,
x = battleLogX,
y = battleLogY,
height = 8,
width = 7,
textureHeight = 8,
textureWidth = 7,
alpha = opacity
)
blitk(
matrixStack = context.pose(),
texture = topRightTexture,
x = battleLogX + battleLogWidth - 13,
y = battleLogY,
height = 8,
width = 13,
textureHeight = 8,
textureWidth = 13,
alpha = opacity
)
blitk(
matrixStack = context.pose(),
texture = bottomLeftTexture,
x = battleLogX,
y = battleLogY + battleLogHeight - 5,
height = 5,
width = 7,
textureHeight = 5,
textureWidth = 7,
alpha = opacity
)
blitk(
matrixStack = context.pose(),
texture = bottomRightTexture,
x = battleLogX + battleLogWidth - 13,
y = battleLogY + battleLogHeight - 10,
height = 10,
width = 13,
textureHeight = 10,
textureWidth = 13,
alpha = opacity
)
blitk(
matrixStack = context.pose(),
texture = topTexture,
x = battleLogX + 7,
y = battleLogY,
height = 7,
width = battleLogWidth - 7 - 13,
textureHeight = 7,
textureWidth = 1,
alpha = opacity
)
blitk(
matrixStack = context.pose(),
texture = bottomTexture,
x = battleLogX + 7,
y = battleLogY + battleLogHeight - 4,
height = 4,
width = battleLogWidth - 7 - 12,
textureHeight = 4,
textureWidth = 1,
alpha = opacity
)
blitk(
matrixStack = context.pose(),
texture = leftTexture,
x = battleLogX,
y = battleLogY + 8,
height = battleLogHeight - 8 - 5,
width = 7,
textureHeight = 1,
textureWidth = 7,
alpha = opacity
)
blitk(
matrixStack = context.pose(),
texture = rightTexture,
x = battleLogX + battleLogWidth - 13,
y = battleLogY + 8,
height = battleLogHeight - 18,
width = 13,
textureHeight = 1,
textureWidth = 13,
alpha = opacity
)
blitk(
matrixStack = context.pose(),
texture = centerTexture,
x = battleLogX + 7,
y = battleLogY + 7,
height = battleLogHeight - 11,
width = battleLogWidth - 20,
textureHeight = battleLogHeight - 11,
textureWidth = battleLogWidth - 20,
alpha = opacity
)
val textBoxHeight = if (expanded) TEXT_BOX_HEIGHT * 2 else TEXT_BOX_HEIGHT
context.enableScissor(
this.x + 5,
appropriateY + 6,
this.x + 5 + width,
appropriateY + 6 + textBoxHeight
x + 5,
round(battleLogY + 6).toInt(),
x + 6 + battleLogWidth,
round(battleLogY + 6 + battleLogHeight).toInt()
)
super.renderWidget(context, mouseX, mouseY, partialTicks)
context.disableScissor()
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val toggleOffsetY = if (expanded) 92 else 46
if (mouseX > (this.x + 160) && mouseX < (this.x + 160 + EXPAND_TOGGLE_SIZE) && mouseY > (appropriateY + toggleOffsetY) && mouseY < (appropriateY + toggleOffsetY + EXPAND_TOGGLE_SIZE)) {
expanded = !expanded
}
updateScrollingState(mouseX, mouseY)
if (scrolling) {
focused = getEntryAtPosition(mouseX, mouseY)
isDragging = true
}
return super.mouseClicked(mouseX, mouseY, button)
}
@@ -183,9 +343,38 @@ class BattleMessagePane(
scrollAmount += deltaY
}
}
if (!tryMove(mouseX, mouseY, deltaX, deltaY)) {
tryAdjustWidth(mouseX, mouseY, button, deltaX, deltaY)
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
private fun tryMove(mouseX: Double, mouseY: Double, deltaX: Double, deltaY: Double): Boolean {
if (mouseY - deltaY < y - 5 || mouseY - deltaY > y + 5) return false
if (mouseX - deltaX < x - 5 || mouseX - deltaX > (x + width + 5)) return false
battleLogX = battleLogX + deltaX
battleLogY = battleLogY + deltaY
return true
}
private fun tryAdjustWidth(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double) {
if (button == 1) return
val frameWidth = battleLogWidth + 16
val frameHeight = battleLogHeight + 9
val expandButtonX1 = frameWidth - 9
val expandButtonX2 = frameWidth - 4
val expandButtonY1 = frameHeight - 9
val expandButtonY2 = frameHeight - 4
if (mouseX - deltaX < x + expandButtonX1 - 15 || mouseX - deltaX > x + expandButtonX2 + 15) return
if (mouseY - deltaY < y + expandButtonY1 - 15 || mouseY - deltaY > y + expandButtonY2 + 15) return
val newHeight = max(mouseY.toInt() + 7 - y, TEXT_BOX_HEIGHT)
val newWidth = max(mouseX.toInt() - x, TEXT_BOX_WIDTH)
battleLogHeight = newHeight
battleLogWidth = newWidth
correctSize()
correctBattleText()
}
private fun updateScrollingState(mouseX: Double, mouseY: Double) {
scrolling = mouseX >= this.scrollbarPosition.toDouble()
&& mouseX < (this.scrollbarPosition + 3).toDouble()
@@ -215,5 +404,9 @@ class BattleMessagePane(
opacity = pane.opacity
)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
return super.mouseClicked(mouseX, mouseY, button)
}
}
}
@@ -14,7 +14,6 @@ import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.battle.BattleGUI
import com.cobblemon.mod.common.client.render.drawScaledText
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Renderable
import net.minecraft.client.gui.components.events.GuiEventListener
import net.minecraft.client.gui.narration.NarratableEntry
import net.minecraft.client.gui.narration.NarratedElementType
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.battle.ClientBattleInformationRepository
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorInformationPacket
import net.minecraft.client.Minecraft
object BattleActorInformationHandler : ClientNetworkPacketHandler<BattleActorInformationPacket> {
override fun handle(packet: BattleActorInformationPacket, client: Minecraft) {
val pokemon = ClientBattleInformationRepository.actors[packet.actor]
if (pokemon == null) return
val previous = pokemon.find { it.uuid == packet.update.uuid }
if (previous == null) return
pokemon.add(pokemon.indexOf(previous), packet.update)
pokemon.remove(previous)
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.battle.ClientBattleInformationRepository
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorTeamPacket
import net.minecraft.client.Minecraft
object BattleActorTeamHandler : ClientNetworkPacketHandler<BattleActorTeamPacket> {
override fun handle(packet: BattleActorTeamPacket, client: Minecraft) {
ClientBattleInformationRepository.actors[packet.actor] = packet.team.toMutableList()
}
}
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.battle
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.battle.ClientBattleInformationRepository
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.client.render.MatrixWrapper
import com.cobblemon.mod.common.net.messages.client.battle.BattleInformationPacket
import com.cobblemon.mod.common.net.messages.client.battle.FieldEffect
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.authlib.minecraft.client.MinecraftClient
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Minecraft
import net.minecraft.resources.ResourceLocation
object BattleInformationHandler : ClientNetworkPacketHandler<BattleInformationPacket> {
override fun handle(packet: BattleInformationPacket, client: Minecraft) {
val currentWeather: FieldEffect? = getCurrentWeather()
ClientBattleInformationRepository.battles[packet.battle] = packet.informationDTO
val newWeather: FieldEffect? = packet.informationDTO.weather
if (newWeather != null && currentWeather?.id != newWeather.id) {
client.executeIfPossible { playWeatherParticle(newWeather, client) }
}
}
private fun getCurrentWeather(): FieldEffect? {
val currentBattleId = CobblemonClient.battle?.battleId ?: return null
val currentBattleInfo = ClientBattleInformationRepository.battles[currentBattleId] ?: return null
return currentBattleInfo.weather
}
private fun playWeatherParticle(weather: FieldEffect, client: Minecraft) {
if (weather.sourceEntityId == null) return
val source = client.level?.getEntity(weather.sourceEntityId) ?: return
val particle = getParticleForWeather(weather) ?: return
val wrapper = MatrixWrapper()
val matrix = PoseStack()
wrapper.updateMatrix(matrix.last().pose())
wrapper.updatePosition(source.position())
val world = Minecraft.getInstance().level ?: return
val effect = BedrockParticleOptionsRepository.getEffect(particle) ?: return
val runtime = MoLangRuntime()
runtime.environment.query.addFunction("is_weather_active") { DoubleValue(getCurrentWeather()?.id == weather.id) }
ParticleStorm(
effect,
wrapper,
wrapper,
world,
runtime = runtime
).spawn()
}
fun getParticleForWeather(weather: FieldEffect): ResourceLocation? {
return when (weather.id) {
"hail" -> cobblemonResource("battle_hail")
"snow" -> cobblemonResource("battle_snow")
"raindance" -> cobblemonResource("battle_rain")
"sandstorm" -> cobblemonResource("battle_sandstorm")
else -> null
}
}
}
@@ -21,11 +21,6 @@ import net.minecraft.locale.Language
object BattleMessageHandler : ClientNetworkPacketHandler<BattleMessagePacket> {
override fun handle(packet: BattleMessagePacket, client: Minecraft) {
val battle = CobblemonClient.battle ?: return
val textRenderer = Minecraft.getInstance().font
for (message in packet.messages) {
val line = message.copy().bold().font(CobblemonResources.DEFAULT_LARGE)
val lines = Language.getInstance().getVisualOrder(textRenderer.splitter.splitLines(line, BattleMessagePane.LINE_WIDTH, line.style))
battle.messages.add(lines)
}
battle.messages.add(packet.messages)
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.battle.ClientBattleInformationRepository
import com.cobblemon.mod.common.net.messages.client.battle.BattleTimerPacket
import net.minecraft.client.Minecraft
object BattleTimerHandler : ClientNetworkPacketHandler<BattleTimerPacket> {
override fun handle(packet: BattleTimerPacket, client: Minecraft) {
ClientBattleInformationRepository.mustChooseBy = packet.mustChooseBy
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.battle.preview.TeamPreviewGUI
import com.cobblemon.mod.common.net.messages.client.battle.CloseTeamPreviewPacket
import net.minecraft.client.Minecraft
object CloseTeamPreviewHandler : ClientNetworkPacketHandler<CloseTeamPreviewPacket> {
override fun handle(packet: CloseTeamPreviewPacket, client: Minecraft) {
if (client.screen is TeamPreviewGUI) {
client.screen!!.onClose()
}
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.battle.preview.ClientBattleTeamPreview
import com.cobblemon.mod.common.client.gui.battle.preview.TeamPreviewGUI
import com.cobblemon.mod.common.net.messages.client.battle.TeamPreviewPacket
import net.minecraft.client.Minecraft
object TeamPreviewHandler : ClientNetworkPacketHandler<TeamPreviewPacket> {
override fun handle(packet: TeamPreviewPacket, client: Minecraft) {
Minecraft.getInstance().setScreen(TeamPreviewGUI(ClientBattleTeamPreview(
packet.selections,
packet.team,
packet.opponent,
packet.mustPickBy,
packet.opponentName
)))
}
}
@@ -33,6 +33,7 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.addPokemonFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.net.serializers.IdentifierDataSerializer
import com.cobblemon.mod.common.api.net.serializers.PlatformTypeDataSerializer
import com.cobblemon.mod.common.api.net.serializers.PoseTypeDataSerializer
import com.cobblemon.mod.common.api.net.serializers.RideBoostsDataSerializer
@@ -63,6 +64,8 @@ import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.types.ElementalTypes
import com.cobblemon.mod.common.api.types.tera.TeraType
import com.cobblemon.mod.common.api.types.tera.TeraTypes
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.BattleBuilder
import com.cobblemon.mod.common.battles.BattleRegistry
@@ -216,7 +219,7 @@ open class PokemonEntity(
@JvmStatic var RIDE_STAMINA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic var SCALE_MODIFIER = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.FLOAT)
@JvmStatic var IS_ALPHA = SynchedEntityData.defineId(PokemonEntity::class.java, EntityDataSerializers.BOOLEAN)
@JvmStatic var TERASTALLIZED = SynchedEntityData.defineId(PokemonEntity::class.java, IdentifierDataSerializer)
const val BATTLE_LOCK = "battle"
const val EVOLUTION_LOCK = "evolving"
@@ -466,6 +469,7 @@ open class PokemonEntity(
builder.define(RIDE_STAMINA, 1F)
builder.define(SCALE_MODIFIER, 1F)
builder.define(IS_ALPHA, false)
builder.define(TERASTALLIZED, cobblemonResource("none"))
}
override fun onSyncedDataUpdated(data: EntityDataAccessor<*>) {
@@ -528,6 +532,23 @@ open class PokemonEntity(
super.handleEntityEvent(status)
}
fun terastallize(type: TeraType?) {
if (type != null) {
entityData.set(TERASTALLIZED, type.id)
setGlowingTag(true)
}
else {
entityData.set(TERASTALLIZED, cobblemonResource("none"))
setGlowingTag(false)
}
}
fun getTerastallization(): TeraType? {
val teraId = entityData.get(TERASTALLIZED)
if (teraId == cobblemonResource("id")) return null
return TeraTypes.get(teraId)
}
override fun sendDebugPackets() {
super.sendDebugPackets()
DebugPackets.sendEntityBrain(this)
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.types.tera.TeraType
import com.cobblemon.mod.common.api.types.tera.TeraTypes
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readItemStack
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.readText
import com.cobblemon.mod.common.util.writeItemStack
import com.cobblemon.mod.common.util.writeString
import com.cobblemon.mod.common.util.writeText
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.world.item.ItemStack
import java.util.*
class BattleActorInformationPacket(val actor: UUID, val update: BattlePokemonDTO): NetworkPacket<BattleActorInformationPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(actor)
buffer.writeUUID(update.uuid)
buffer.writeBoolean(update.fainted)
buffer.writeNullable (update.ability) { _, v -> buffer.writeString(v) }
buffer.writeCollection(update.moves) { _, v ->
buffer.writeNullable(v) { _, dto ->
buffer.writeText(dto.move)
buffer.writeInt(dto.timesUsed)
}
}
buffer.writeNullable(update.heldItem) { _, item -> buffer.writeItemStack(item) }
buffer.writeMap(update.buffs, { _, v -> buffer.writeEnum(v) }, { _, v -> buffer.writeDouble(v) })
buffer.writeNullable(update.speed) { _, v -> buffer.writeInt(v) }
buffer.writeNullable(update.terastallized) { _, v -> buffer.writeResourceLocation(v.id) }
buffer.writeNullable(update.activeBattlePokemonDTO) { _, v -> v.saveToBuffer(buffer) }
}
companion object {
val ID = cobblemonResource("battle_actor_information")
fun decode(buffer: RegistryFriendlyByteBuf) = BattleActorInformationPacket(
actor = buffer.readUUID(),
update = BattlePokemonDTO(
uuid = buffer.readUUID(),
fainted = buffer.readBoolean(),
ability = buffer.readNullable { buffer.readString() },
moves = buffer.readList { buffer.readNullable { MoveDTO(buffer.readText(), buffer.readInt()) } },
heldItem = buffer.readNullable { buffer.readItemStack() },
buffs = buffer.readMap({ buffer.readEnum(Stats::class.java) }, { buffer.readDouble() }),
speed = buffer.readNullable { buffer.readInt() },
terastallized = buffer.readNullable { TeraTypes.get(buffer.readResourceLocation()) },
activeBattlePokemonDTO = buffer.readNullable { BattleInitializePacket.ActiveBattlePokemonDTO.loadFromBuffer(buffer) }
)
)
}
}
data class BattlePokemonDTO(
val uuid: UUID,
val fainted: Boolean,
val ability: String?,
val moves: List<MoveDTO?>,
val heldItem: ItemStack?,
val buffs: Map<Stats, Double>,
val speed: Int?,
val terastallized: TeraType?,
val activeBattlePokemonDTO: BattleInitializePacket.ActiveBattlePokemonDTO?
)
data class MoveDTO(
val move: Component,
var timesUsed: Int
)
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.types.tera.TeraTypes
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readItemStack
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.readText
import com.cobblemon.mod.common.util.writeItemStack
import com.cobblemon.mod.common.util.writeString
import com.cobblemon.mod.common.util.writeText
import net.minecraft.network.RegistryFriendlyByteBuf
import java.util.UUID
class BattleActorTeamPacket(val actor: UUID, val team: List<BattlePokemonDTO>): NetworkPacket<BattleActorTeamPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(actor)
buffer.writeCollection(team) { pb, value ->
pb.writeUUID(value.uuid)
pb.writeBoolean(value.fainted)
buffer.writeNullable (value.ability) { _, v -> buffer.writeString(v) }
pb.writeCollection(value.moves) { _, v ->
pb.writeNullable(v) { _, dto ->
buffer.writeText(dto.move)
buffer.writeInt(dto.timesUsed)
}
}
pb.writeNullable(value.heldItem) { _, item -> buffer.writeItemStack(item) }
pb.writeMap(value.buffs, { _, v -> buffer.writeEnum(v) }, { _, v -> buffer.writeDouble(v) })
buffer.writeNullable(value.speed) { _, v -> buffer.writeInt(v) }
buffer.writeNullable(value.terastallized) { _, v -> buffer.writeResourceLocation(v.id) }
buffer.writeNullable(value.activeBattlePokemonDTO) { _, v -> v.saveToBuffer(buffer) }
}
}
companion object {
val ID = cobblemonResource("battle_team")
fun decode(buffer: RegistryFriendlyByteBuf) = BattleActorTeamPacket(
actor = buffer.readUUID(),
team = buffer.readList {
BattlePokemonDTO(
uuid = buffer.readUUID(),
fainted = buffer.readBoolean(),
ability = buffer.readNullable { buffer.readString() },
moves = buffer.readList { buffer.readNullable { MoveDTO(buffer.readText(), buffer.readInt()) } },
heldItem = buffer.readNullable { buffer.readItemStack() },
buffs = buffer.readMap({ buffer.readEnum(Stats::class.java) }, { buffer.readDouble() }),
speed = buffer.readNullable { buffer.readInt() },
terastallized = buffer.readNullable { TeraTypes.get(buffer.readResourceLocation()) },
activeBattlePokemonDTO = buffer.readNullable { BattleInitializePacket.ActiveBattlePokemonDTO.loadFromBuffer(buffer) }
)
}
)
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readNullable
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeNullable
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import java.time.Instant
import java.util.UUID
class BattleInformationPacket(val battle: UUID, val informationDTO: BattleInformationDTO): NetworkPacket<BattleInformationPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(battle)
buffer.writeInt(informationDTO.turn)
buffer.writeNullable(informationDTO.weather) { _, value -> encodeFieldEffect(buffer, value) }
buffer.writeNullable(informationDTO.terrain) { _, value -> encodeFieldEffect(buffer, value) }
buffer.writeNullable(informationDTO.room) { _, value -> encodeFieldEffect(buffer, value) }
buffer.writeCollection(informationDTO.side1SidedEffects) { _, value -> encodeFieldEffect(buffer, value) }
buffer.writeCollection(informationDTO.side2SidedEffects) { _, value -> encodeFieldEffect(buffer, value) }
}
companion object {
val ID = cobblemonResource("battle_information")
fun decode(buffer: RegistryFriendlyByteBuf) = BattleInformationPacket(
battle = buffer.readUUID(),
informationDTO = BattleInformationDTO(
turn = buffer.readInt(),
weather = buffer.readNullable { decodeFieldEffect(buffer) },
terrain = buffer.readNullable { decodeFieldEffect(buffer) },
room = buffer.readNullable { decodeFieldEffect(buffer) },
side1SidedEffects = buffer.readList { decodeFieldEffect(buffer) },
side2SidedEffects = buffer.readList { decodeFieldEffect(buffer) }
)
)
private fun encodeFieldEffect(buffer: RegistryFriendlyByteBuf, fieldEffect: FieldEffect) {
buffer.writeString(fieldEffect.id)
buffer.writeInt(fieldEffect.turnStarted)
buffer.writeNullable(fieldEffect.sourceEntityId) { _, value -> buffer.writeInt(value) }
}
private fun decodeFieldEffect(buffer: RegistryFriendlyByteBuf): FieldEffect {
return FieldEffect(
id = buffer.readString(),
turnStarted = buffer.readInt(),
sourceEntityId = buffer.readNullable { buffer.readInt() }
)
}
}
}
data class BattleInformationDTO(
val turn: Int,
val weather: FieldEffect?,
val terrain: FieldEffect?,
val room: FieldEffect?,
val side1SidedEffects: List<FieldEffect>,
val side2SidedEffects: List<FieldEffect>
)
data class FieldEffect(
val id: String,
val turnStarted: Int,
val sourceEntityId: Int?
)
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import java.time.Instant
class BattleTimerPacket(val mustChooseBy: Instant?) : NetworkPacket<BattleTimerPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeNullable(mustChooseBy) { _, value -> buffer.writeInstant(value) }
}
companion object {
val ID = cobblemonResource("battle_timer")
fun decode(buffer: RegistryFriendlyByteBuf) = BattleTimerPacket(
mustChooseBy = buffer.readNullable { buffer.readInstant() }
)
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
class CloseTeamPreviewPacket : NetworkPacket<CloseTeamPreviewPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
}
companion object {
val ID = cobblemonResource("close_team_preview")
fun decode(buffer: RegistryFriendlyByteBuf): CloseTeamPreviewPacket {
return CloseTeamPreviewPacket()
}
}
}
@@ -0,0 +1,122 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.types.tera.TeraTypes
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.net.messages.client.battle.BattleInitializePacket.ActiveBattlePokemonDTO
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readItemStack
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.readText
import com.cobblemon.mod.common.util.writeItemStack
import com.cobblemon.mod.common.util.writeString
import com.cobblemon.mod.common.util.writeText
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.chat.Component
import java.time.Instant
class TeamPreviewPacket : NetworkPacket<TeamPreviewPacket> {
override val id = ID
val selections: Int
val team: List<BattlePokemonDTO?>
val opponent: List<BattlePokemonDTO?>
val mustPickBy: Instant
val opponentName: Component
constructor(
selections: Int,
team: List<BattlePokemonDTO>,
opponent: List<BattlePokemonDTO>,
mustPickBy: Instant,
opponentName: Component
) {
this.selections = selections
this.team = team
this.opponent = opponent
this.mustPickBy = mustPickBy
this.opponentName = opponentName
}
constructor(
selections: Int,
team: List<Pokemon?>,
opponent: List<Pokemon?>,
openTeamSheet: Boolean,
mustPickBy: Instant,
opponentName: Component
) {
this.selections = selections
this.team = team.map { it?.let { BattlePokemon(it).toBattleDTO(true) } }
this.opponent = opponent.map { it?.let { BattlePokemon(it).toBattleDTO(openTeamSheet) } }
this.mustPickBy = mustPickBy
this.opponentName = opponentName
}
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(selections)
buffer.writeCollection(team) { _, value ->
buffer.writeNullable(value) { _, value -> encodeBattlePokemonDTO(buffer, value) }
}
buffer.writeCollection(opponent) { _, value ->
buffer.writeNullable(value) { _, value -> encodeBattlePokemonDTO(buffer, value) }
}
buffer.writeInstant(mustPickBy)
buffer.writeText(opponentName)
}
private fun encodeBattlePokemonDTO(buffer: RegistryFriendlyByteBuf, dto: BattlePokemonDTO) {
buffer.writeUUID(dto.uuid)
buffer.writeBoolean(dto.fainted)
buffer.writeNullable(dto.ability) { _, v -> buffer.writeString(v) }
buffer.writeCollection(dto.moves) { _, v ->
buffer.writeNullable(v) { _, dto ->
buffer.writeText(dto.move)
buffer.writeInt(dto.timesUsed)
}
}
buffer.writeNullable(dto.heldItem) { _, item -> buffer.writeItemStack(item) }
buffer.writeMap(dto.buffs, { _, v -> buffer.writeEnum(v) }, { _, v -> buffer.writeDouble(v) })
buffer.writeNullable(dto.speed) { _, v -> buffer.writeInt(v) }
buffer.writeNullable(dto.terastallized) { _, v -> buffer.writeResourceLocation(v.id) }
buffer.writeNullable(dto.activeBattlePokemonDTO) { _, v -> v.saveToBuffer(buffer) }
}
companion object {
val ID = cobblemonResource("team_preview")
fun decode(buffer: RegistryFriendlyByteBuf): TeamPreviewPacket {
return TeamPreviewPacket(
buffer.readInt(),
buffer.readList { buffer.readNullable { decodeBattlePokemonDTO(buffer) } },
buffer.readList { buffer.readNullable { decodeBattlePokemonDTO(buffer) } },
buffer.readInstant(),
buffer.readText()
)
}
private fun decodeBattlePokemonDTO(buffer: RegistryFriendlyByteBuf): BattlePokemonDTO {
return BattlePokemonDTO(
uuid = buffer.readUUID(),
fainted = buffer.readBoolean(),
ability = buffer.readNullable { buffer.readString() },
moves = buffer.readList { buffer.readNullable { MoveDTO(buffer.readText(), buffer.readInt()) } },
heldItem = buffer.readNullable { buffer.readItemStack() },
buffs = buffer.readMap({ buffer.readEnum(Stats::class.java) }, { buffer.readDouble() }),
speed = buffer.readNullable { buffer.readInt() },
terastallized = buffer.readNullable { TeraTypes.get(buffer.readResourceLocation()) },
activeBattlePokemonDTO = buffer.readNullable { ActiveBattlePokemonDTO.loadFromBuffer(buffer) }
)
}
}
}
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
class SpectateNearestBattlePacket : NetworkPacket<SpectateNearestBattlePacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {}
companion object {
val ID = cobblemonResource("battle_spectate_nearest")
fun decode(buffer: RegistryFriendlyByteBuf) = SpectateNearestBattlePacket()
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.battle
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import java.util.UUID
class TeamPreviewSelectPokemonPacket(val selected: Set<Int>) : NetworkPacket<TeamPreviewSelectPokemonPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeCollection(selected) { _, value -> buffer.writeInt(value) }
}
companion object {
val ID = cobblemonResource("team_preview_select_pokemon")
fun decode(buffer: RegistryFriendlyByteBuf) = TeamPreviewSelectPokemonPacket(
buffer.readList { buffer.readInt() }.toSet()
)
}
}
@@ -15,8 +15,6 @@ import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.net.messages.client.battle.BattleInitializePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMusicPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.util.*
@@ -40,10 +38,10 @@ object SpectateBattleHandler : ServerNetworkPacketHandler<SpectateBattlePacket>
// Check los and range
val targetedPlayerEntity = packet.targetedEntityId.getPlayer() ?: return
if (player.traceFirstEntityCollision(
entityClass = LivingEntity::class.java,
ignoreEntity = player,
maxDistance = Cobblemon.config.battleSpectateMaxDistance,
collideBlock = ClipContext.Fluid.NONE) != targetedPlayerEntity) {
entityClass = LivingEntity::class.java,
ignoreEntity = player,
maxDistance = Cobblemon.config.battleSpectateMaxDistance,
collideBlock = ClipContext.Fluid.NONE) != targetedPlayerEntity) {
player.sendSystemMessage(lang("ui.interact.failed").yellow())
return
}
@@ -75,10 +73,7 @@ object SpectateBattleHandler : ServerNetworkPacketHandler<SpectateBattlePacket>
}
val battle = BattleRegistry.getBattleByParticipatingPlayer(target) ?: return
battle.spectators.add(player.uuid)
player.sendPacket(BattleInitializePacket(battle, null))
player.sendPacket(BattleMessagePacket(battle.chatLog))
battle.startSpectating(player)
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.serverhandling.battle
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.BattleRegistry
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.net.messages.client.battle.BattleMusicPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateNearestBattlePacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import org.apache.logging.log4j.LogManager
object SpectateNearestBattleHandler : ServerNetworkPacketHandler<SpectateNearestBattlePacket> {
val LOGGER = LogManager.getLogger()
override fun handle(
packet: SpectateNearestBattlePacket,
server: MinecraftServer,
player: ServerPlayer
) {
if (BattleRegistry.getBattleBySpectatorId(player.uuid) != null) return
if (BattleRegistry.getBattleByParticipatingPlayer(player) != null) return
val nearest = BattleRegistry.getPlayersCurrentlyBattling()
.filter { it != player && it.serverLevel() == player.serverLevel() && it.distanceTo(player) <= 64 }
.minByOrNull { it.distanceTo(player) }
if (nearest == null) return
val battle = BattleRegistry.getBattleByParticipatingPlayerId(nearest.uuid)
if (battle != null && Cobblemon.config.allowSpectating) {
val target = battle.actors.filterIsInstance<PlayerBattleActor>().firstOrNull { it.uuid == nearest.uuid }
battle.startSpectating(player)
target?.battleTheme?.let { player.sendPacket(BattleMusicPacket(it)) }
}
else {
LOGGER.error("Battle of player id ${nearest.uuid} not found (${player.uuid} tried spectating)")
}
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.serverhandling.battle
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.battles.TeamPreviewManager
import com.cobblemon.mod.common.net.messages.server.battle.TeamPreviewSelectPokemonPacket
import com.cobblemon.mod.common.util.safeParty
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object TeamPreviewSelectPokemonHandler : ServerNetworkPacketHandler<TeamPreviewSelectPokemonPacket> {
override fun handle(packet: TeamPreviewSelectPokemonPacket, server: MinecraftServer, player: ServerPlayer) {
val party = player.safeParty()?.toGappyList() ?: return
for (slot in packet.selected) {
if (slot < 0 || slot >= party.size) return
if (party[slot] == null) return
}
TeamPreviewManager.changeSelections(player.uuid, packet.selected)
}
}
@@ -557,9 +557,9 @@ open class Pokemon : ShowdownIdentifiable {
* that they had last.
*/
var benchedMoves = BenchedMoves().also { it.changeFunction = {
onChange(BenchedMovesUpdatePacket({ this }, it))
this.getOwnerUUID()?.let { it1 -> Cobblemon.playerDataManager.getTMData(it1)?.syncTMsFromPokemon(this) } // we want to make sure to update the players TM Data store when any moves are learned
}
onChange(BenchedMovesUpdatePacket({ this }, it))
this.getOwnerUUID()?.let { it1 -> Cobblemon.playerDataManager.getTMData(it1)?.syncTMsFromPokemon(this) } // we want to make sure to update the players TM Data store when any moves are learned
}
}
internal set(value) {
val oldChangeFunction = field.changeFunction
@@ -791,6 +791,7 @@ open class Pokemon : ShowdownIdentifiable {
return this.form.showdownId()
}
fun sendOut(level: ServerLevel, position: Vec3, illusion: IllusionEffect?, mutation: (PokemonEntity) -> Unit = {}): PokemonEntity? {
CobblemonEvents.POKEMON_SENT_PRE.postThen(PokemonSentEvent.Pre(this, level, position)) {
SeasonFeatureHandler.updateSeason(this, level, position.toBlockPos())
@@ -807,6 +808,7 @@ open class Pokemon : ShowdownIdentifiable {
return null
}
fun sendOutWithAnimation(
source: LivingEntity,
level: ServerLevel,
@@ -883,7 +885,7 @@ open class Pokemon : ShowdownIdentifiable {
it.battleId = battleId
if (it.battleId == null) {
it.platform = PlatformType.NONE
it.platform = PlatformType.NONE
}
it.after(seconds = THROW_DURATION) {
@@ -1474,7 +1476,7 @@ open class Pokemon : ShowdownIdentifiable {
this.ivs[it.key] = it.value
}
other.ivs.hyperTrainedIVs.forEach {
(stat, value) -> this.ivs.setHyperTrainedIV(stat, value)
(stat, value) -> this.ivs.setHyperTrainedIV(stat, value)
}
}
this.evs.doWithoutEmitting {
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.api.dialogue.DialogueManager
import com.cobblemon.mod.common.api.reactive.Observable.Companion.filter
import com.cobblemon.mod.common.api.reactive.Observable.Companion.takeFirst
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.TeamManager
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.RequestPCBoxWallpapersPacket
@@ -58,6 +59,13 @@ import kotlin.math.sin
// Stuff like getting their party
fun ServerPlayer.party() = Cobblemon.storage.getParty(this)
fun ServerPlayer.safeParty(): PlayerPartyStore? {
return try {
Cobblemon.storage.getParty(this)
} catch (e: Exception) {
null
}
}
fun ServerPlayer.pc() = Cobblemon.storage.getPC(this)
fun ServerPlayer.pokedex() = Cobblemon.playerDataManager.getPokedexData(this)
fun ServerPlayer.tmList() = Cobblemon.playerDataManager.getTMData(this)
@@ -2237,6 +2237,66 @@
"cobblemon.battle.ui.forfeit_confirmation": "Forfeiting makes you lose the battle. Are you sure?",
"cobblemon.battle.ui.hide_label": "Press %1$s to move around.",
"cobblemon.battle.ui.actions_label": "You need to choose an action. Press %1$s.",
"cobblemon.battle.ui.label.buffs": "Buffs/Debuffs",
"cobblemon.battle.ui.label.ability": "Ability",
"cobblemon.battle.ui.label.moves": "Moves",
"cobblemon.battle.ui.label.held_item": "Held Item",
"cobblemon.battle.ui.label.speed_tier": "Speed Tier",
"cobblemon.battle.ui.label.pokemon_form": "Pokémon Form",
"cobblemon.battle.ui.effectiveness.has_no_effect": "Has no effect",
"cobblemon.battle.ui.effectiveness.mostly_ineffective": "Mostly ineffective",
"cobblemon.battle.ui.effectiveness.not_very_effective": "Not very effective",
"cobblemon.battle.ui.effectiveness.effective": "Effective",
"cobblemon.battle.ui.effectiveness.super_effective": "Super effective",
"cobblemon.battle.ui.effectiveness.extremely_effective": "Extremely effective",
"cobblemon.battle.ui.turn": "Turn %1$s",
"cobblemon.battle.ui.turn.timer": "Turn %1$s (%2$s)",
"cobblemon.battle.ui.stats.special_attack": "Sp. Attack",
"cobblemon.battle.ui.stats.special_defence": "Sp. Defense",
"cobblemon.battle.ui.speed_tier": "%1$s to %2$s",
"cobblemon.battle.ui.effect.raindance": "Rain — %1$s turn(s) left",
"cobblemon.battle.ui.effect.raindance.range": "Rain — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.primordialsea": "Heavy Rain",
"cobblemon.battle.ui.effect.sandstorm": "Sandstorm — %1$s turn(s) left",
"cobblemon.battle.ui.effect.sandstorm.range": "Sandstorm — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.sunnyday": "Sun — %1$s turn(s) left",
"cobblemon.battle.ui.effect.sunnyday.range": "Sun — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.desolateland": "Intense Sun",
"cobblemon.battle.ui.effect.hail": "Hail — %1$s turn(s) left",
"cobblemon.battle.ui.effect.hail.range": "Hail — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.snow": "Snow — %1$s turn(s) left",
"cobblemon.battle.ui.effect.snow.range": "Snow — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.deltastream": "Delta Stream",
"cobblemon.battle.ui.effect.trickroom": "Trick Room — %1$s turn(s) left",
"cobblemon.battle.ui.effect.magicroom": "Magic Room — %1$s turn(s) left",
"cobblemon.battle.ui.effect.wonderroom": "Wonder Room — %1$s turn(s) left",
"cobblemon.battle.ui.effect.electricterrain": "Electric Terrain — %1$s turn(s) left",
"cobblemon.battle.ui.effect.electricterrain.range": "Electric Terrain — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.mistyterrain": "Misty Terrain — %1$s turn(s) left",
"cobblemon.battle.ui.effect.mistyterrain.range": "Misty Terrain — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.psychicterrain": "Psychic Terrain — %1$s turn(s) left",
"cobblemon.battle.ui.effect.psychicterrain.range": "Psychic Terrain — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.grassyterrain": "Grassy Terrain — %1$s turn(s) left",
"cobblemon.battle.ui.effect.grassyterrain.range": "Grassy Terrain — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.corrosiveterrain": "Corrosive Terrain — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.effect.corrosiveterrain.range": "Corrosive Terrain — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.sided_effect.sharpsteel": "Sharp Steel",
"cobblemon.battle.ui.sided_effect.spikes": "Spikes",
"cobblemon.battle.ui.sided_effect.stealthrock": "Stealth Rock",
"cobblemon.battle.ui.sided_effect.stickyweb": "Sticky Web",
"cobblemon.battle.ui.sided_effect.toxicspikes": "Toxic Spikes",
"cobblemon.battle.ui.sided_effect.reflect": "Reflect — %1$s turn(s) left",
"cobblemon.battle.ui.sided_effect.reflect.range": "Reflect — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.sided_effect.lightscreen": "Light Screen — %1$s turn(s) left",
"cobblemon.battle.ui.sided_effect.lightscreen.range": "Light Screen — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.sided_effect.auroraveil": "Aurora Veil — %1$s turn(s) left",
"cobblemon.battle.ui.sided_effect.auroraveil.range": "Aurora Veil — %1$s to %2$s turn(s) left",
"cobblemon.battle.ui.sided_effect.tailwind": "Tailwind — %1$s turn(s) left",
"cobblemon.battle.ui.team_info": "Team Information",
"cobblemon.battle.ui.team_info.button": "Info",
"cobblemon.battle.ui.team_preview.title": "Team Preview",
"cobblemon.battle.ui.team_info.title": "Team Info",
"cobblemon.battle.ui.team_preview.waiting": "Waiting on opponent%1$s",
"cobblemon.ui.evolve": "Evolve",
"cobblemon.ui.evolution": "Evolution",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

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