Compare commits

...
178 changed files with 7470 additions and 156 deletions
+3
View File
@@ -45,3 +45,6 @@ common/.fabric
# MacOS
.DS_Store
/common/config/*
# Temp Files
/temp/
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.battles.TeamPreviewManager
import com.cobblemon.mod.common.CobblemonBuildDetails.smallCommitHash
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.SeasonResolver
@@ -254,6 +256,7 @@ object Cobblemon {
}
PlatformEvents.SERVER_PLAYER_LOGOUT.subscribe {
PCLinkManager.removeLink(it.player.uuid)
TeamPreviewManager.remove(it.player)
BattleRegistry.onPlayerDisconnect(it.player)
storage.onPlayerDisconnect(it.player)
playerDataManager.onPlayerDisconnect(it.player)
@@ -450,6 +453,7 @@ object Cobblemon {
}
PlatformEvents.SERVER_STOPPED.subscribe {
TeamPreviewManager.clear()
storage.unregisterAll(it.server.registryAccess())
playerDataManager.saveAllStores()
playerDataManager.saveExecutor.shutdown()
@@ -8,6 +8,34 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.net.serverhandling.battle.TeamPreviewSelectPokemonHandler
import com.cobblemon.mod.common.net.messages.server.battle.TeamPreviewSelectPokemonPacket
import com.cobblemon.mod.common.client.net.battle.CloseTeamPreviewHandler
import com.cobblemon.mod.common.net.messages.client.battle.CloseTeamPreviewPacket
import com.cobblemon.mod.common.client.net.battle.TeamPreviewHandler
import com.cobblemon.mod.common.net.messages.client.battle.TeamPreviewPacket
import com.cobblemon.mod.common.client.net.battle.BattleTimerHandler
import com.cobblemon.mod.common.net.messages.client.battle.BattleTimerPacket
import com.cobblemon.mod.common.client.net.battle.BattleInformationHandler
import com.cobblemon.mod.common.net.messages.client.battle.BattleInformationPacket
import com.cobblemon.mod.common.client.net.battle.BattleActorTeamHandler
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorTeamPacket
import com.cobblemon.mod.common.client.net.battle.BattleActorInformationHandler
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorInformationPacket
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.OpenBehaviourEditorHandler
import com.cobblemon.mod.common.client.net.PlayerInteractOptionsHandler
@@ -386,6 +414,12 @@ object CobblemonNetwork {
// Battle packets
list.add(PacketRegisterInfo(BattleEndPacket.ID, BattleEndPacket::decode, BattleEndHandler))
list.add(PacketRegisterInfo(BattleInitializePacket.ID, BattleInitializePacket::decode, BattleInitializeHandler))
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))
list.add(PacketRegisterInfo(BattleQueueRequestPacket.ID, BattleQueueRequestPacket::decode, BattleQueueRequestHandler))
list.add(PacketRegisterInfo(BattleFaintPacket.ID, BattleFaintPacket::decode, BattleFaintHandler))
list.add(PacketRegisterInfo(BattleMakeChoicePacket.ID, BattleMakeChoicePacket::decode, BattleMakeChoiceHandler))
@@ -573,6 +607,7 @@ object CobblemonNetwork {
// Battle packets
list.add(PacketRegisterInfo(BattleSelectActionsPacket.ID, BattleSelectActionsPacket::decode, BattleSelectActionsHandler))
list.add(PacketRegisterInfo(TeamPreviewSelectPokemonPacket.ID, TeamPreviewSelectPokemonPacket::decode, TeamPreviewSelectPokemonHandler))
list.add(PacketRegisterInfo(SpectateBattlePacket.ID, SpectateBattlePacket::decode, SpectateBattleHandler))
list.add(PacketRegisterInfo(RemoveSpectatorPacket.ID, RemoveSpectatorPacket::decode, RemoveSpectatorHandler))
@@ -8,6 +8,16 @@
package com.cobblemon.mod.common.api.battles.model
import com.cobblemon.mod.common.net.messages.client.battle.FieldEffect
import com.cobblemon.mod.common.net.messages.client.battle.BattleInformationPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleInformationDTO
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
@@ -247,6 +257,38 @@ open class PokemonBattle(
}
}
this.turn = newTurnNumber
notifyAllOfUpdates()
}
fun notifyAllOfUpdates() {
val uuids = this.actors.flatMap { it.getPlayerUUIDs() } + 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() {
@@ -454,6 +496,7 @@ open class PokemonBattle(
}
fun tick() {
if (!ended) actors.filterIsInstance<PlayerBattleActor>().forEach { it.timer?.tick() }
try {
while (dispatchResult.canProceed()) {
val dispatch = dispatches.poll() ?: break
@@ -8,6 +8,10 @@
package com.cobblemon.mod.common.api.battles.model.actor
import com.cobblemon.mod.common.net.messages.client.battle.BattleTimerPacket
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.net.NetworkPacket
@@ -48,6 +52,20 @@ abstract class BattleActor(
var responses = mutableListOf<ShowdownActionResponse>()
val expectingPassActions = mutableListOf<ShowdownActionResponse>()
var mustChoose = false
set(value) {
if (field == value) return
field = value
if (this is PlayerBattleActor) {
timer?.let {
if (value) {
it.startSelection()
sendUpdate(BattleTimerPacket(it.mustChooseBy()))
} else {
it.endSelection()
}
}
}
}
var itemsUsed = mutableListOf<BagItem>()
/** For when battles start, it's the number of Pokémon that are still in the process of being sent out (animation wise) */
@@ -129,7 +129,12 @@ class ParticleEvent(
onDespawn = {},
getParticleColor = storm.getParticleColor,
targetPos = storm.targetPos,
runtime = MoLangRuntime().setup(),
runtime = MoLangRuntime().setup().also { runtime ->
// Weather sub-emitters share the parent's lifetime condition.
storm.runtime.environment.query.functions["is_weather_active"]?.let {
runtime.environment.query.addFunction("is_weather_active", it)
}
},
entity = storm.entity
)
//The reason this doesn't use the newStorms runtime is that the parent storm can add queries AFTER
@@ -211,4 +216,4 @@ class EventSoundEffect(
).apply(instance, ::EventSoundEffect)
}
}
}
}
@@ -28,7 +28,11 @@ 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 hasTeamPreview: Boolean
get() = ruleSet.any { it.equals(BattleRules.TEAM_PREVIEW, ignoreCase = true) || it.equals("teampreview", ignoreCase = true) }
companion object {
fun setBattleRules(
battleFormat: BattleFormat,
@@ -121,4 +125,4 @@ data class BattleFormat(
}
""".trimIndent().replace("\n", "")
}
}
}
@@ -211,7 +211,16 @@ object ShowdownInterpreter {
}
// Broadcasts a generic lang to notify players of ability activations (effects are broadcasted separately)
fun broadcastAbility(battle: PokemonBattle, effect: Effect, pokemon: BattlePokemon) {
@JvmOverloads
fun broadcastAbility(battle: PokemonBattle, effect: Effect, pokemon: BattlePokemon, revealedAbility: Effect = effect) {
// Direct -ability messages omit the ability: prefix. A copied ability can also differ
// from the effect announced in chat (for example, Trace).
if (revealedAbility.type == Effect.Type.ABILITY || revealedAbility.type == Effect.Type.PURE) {
battle.dispatchGo {
pokemon.revealedAbility = revealedAbility.id
pokemon.sendUpdate()
}
}
battle.dispatchWaiting(0.5F) {
val lang = battleLang("ability.generic", pokemon.getName(), effect.typelessData).yellow()
battle.broadcastChatMessage(lang)
@@ -321,4 +330,4 @@ object ShowdownInterpreter {
fun registerSideInstructionParser(id: String, callback: (PokemonBattle, BattleActor, InstructionSet, BattleMessage) -> InterpreterInstruction) {
sideInstructionParser[id] = callback
}
}
}
@@ -0,0 +1,151 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.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 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) {
require(sideA.uuid != sideB.uuid) { "A team preview requires two distinct participants" }
require(listOf(sideA, sideB).all { it.neededSelections in 1..it.party.count { pokemon -> pokemon != null } }) {
"Each side must be able to fill its requested selection"
}
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 clear() {
previews.toList().forEach { cancel(it) }
}
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) {
if (!previews.remove(preview)) return
if (preview.sideA.isPlayer) {
preview.sideA.uuid.getPlayer()?.sendPacket(CloseTeamPreviewPacket())
}
if (preview.sideB.isPlayer) {
preview.sideB.uuid.getPlayer()?.sendPacket(CloseTeamPreviewPacket())
}
preview.onCancel(preview)
}
fun start(preview: TeamPreview) {
if (!previews.remove(preview)) return
if (preview.sideA.isPlayer) {
preview.sideA.uuid.getPlayer()?.sendPacket(CloseTeamPreviewPacket())
}
if (preview.sideB.isPlayer) {
preview.sideB.uuid.getPlayer()?.sendPacket(CloseTeamPreviewPacket())
}
preview.onStart(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
val side = if (isSideA) preview.sideA else preview.sideB
if (!side.isPlayer || !isValidSelection(side.party, selections, side.neededSelections, requireComplete = false)) return
previews.remove(preview)
if (isSideA) {
previews.add(
preview.copy(
sideA = preview.sideA.copy(selections = selections.toSet()),
)
)
}
else {
previews.add(
preview.copy(
sideB = preview.sideB.copy(selections = selections.toSet()),
)
)
}
}
fun hasEligibleSelections(side: TeamPreviewSide): Boolean =
isValidSelection(side.party, side.selections, side.neededSelections)
internal fun isValidSelection(party: List<*>, selections: Set<Int>, needed: Int, requireComplete: Boolean = true): Boolean {
if (needed < 1 || selections.size > needed || (requireComplete && selections.size != needed)) return false
return selections.all { it in party.indices && party[it] != null }
}
}
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)
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.battles.actor
import com.cobblemon.mod.common.battles.timers.PlayerBattleTimer
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
@@ -33,6 +35,8 @@ class PlayerBattleActor(
pokemonList: List<BattlePokemon>,
) : BattleActor(uuid, pokemonList.toMutableList()), EntityBackedBattleActor<ServerPlayer> {
var timer: PlayerBattleTimer? = null
override val initialPos: Vec3?
override val entity: ServerPlayer?
get() = this.uuid.getPlayer()
@@ -37,7 +37,7 @@ class AbilityInstruction(val instructionSet: InstructionSet, val message: Battle
val optionalPokemon = message.battlePokemonFromOptional(battle)
// If there is an optional effect causing the activation, broadcast that instead of the standard effect
ShowdownInterpreter.broadcastAbility(battle, optionalEffect ?: effect, pokemon)
ShowdownInterpreter.broadcastAbility(battle, optionalEffect ?: effect, pokemon, revealedAbility = effect)
battle.dispatch {
val pokemonName = pokemon.getName()
@@ -63,4 +63,4 @@ class AbilityInstruction(val instructionSet: InstructionSet, val message: Battle
else return@dispatch GO
}
}
}
}
@@ -83,6 +83,10 @@ class BoostInstruction(battle: PokemonBattle, val message: BattleMessage, val is
}
battle.broadcastChatMessage(lang)
val changedStat = Stats.getStat(statKey)
val delta = if (isBoost) stages else -stages
pokemon.statChanges[changedStat] = ((pokemon.statChanges[changedStat] ?: 0) + delta).coerceIn(-6, 6)
pokemon.sendUpdate()
val boostBucket = if (isBoost) BattleContext.Type.BOOST else BattleContext.Type.UNBOOST
val context = ShowdownInterpreter.getContextFromAction(message, boostBucket, battle)
// TODO: replace with context that tracks detailed information such as # of stages
@@ -26,6 +26,8 @@ class ClearAllBoostInstruction(val message: BattleMessage): InterpreterInstructi
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
battle.activePokemon.forEach {
it.battlePokemon?.statChanges?.clear()
it.battlePokemon?.sendUpdate()
it.battlePokemon?.contextManager?.clear(BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
}
battle.broadcastChatMessage(battleLang("clearallboost"))
@@ -30,6 +30,8 @@ class ClearBoostInstruction(val message: BattleMessage): InterpreterInstruction
val pokemonName = battlePokemon.getName()
val lang = battleLang("clearboost", pokemonName)
battle.broadcastChatMessage(lang)
battlePokemon.statChanges.clear()
battlePokemon.sendUpdate()
battlePokemon.contextManager.clear(BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
battle.minorBattleActions[battlePokemon.uuid] = message
}
@@ -36,6 +36,8 @@ class ClearNegativeBoostInstruction(val message: BattleMessage): InterpreterInst
battle.broadcastChatMessage(lang)
}
battlePokemon.statChanges.entries.removeIf { it.value < 0 }
battlePokemon.sendUpdate()
battlePokemon.contextManager.clear(BattleContext.Type.UNBOOST)
battle.minorBattleActions[battlePokemon.uuid] = message
}
@@ -32,6 +32,10 @@ class CopyBoostInstruction(val message: BattleMessage): InterpreterInstruction {
val lang = battleLang("copyboost.generic", pokemonName, targetPokemonName)
battle.broadcastChatMessage(lang)
val changes = targetPokemon.statChanges.toMap()
pokemon.statChanges.clear()
pokemon.statChanges.putAll(changes)
pokemon.sendUpdate()
pokemon.contextManager.copy(targetPokemon.contextManager, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
battle.minorBattleActions[pokemon.uuid] = message
}
@@ -155,6 +155,13 @@ class DamageInstruction(
val newHealthRatio: Float
val remainingHealth = newHealth.split("/")[0].toInt()
val previousHealth = battlePokemon.effectedPokemon.currentHealth
val damageDealt = (previousHealth - remainingHealth).coerceAtLeast(0)
val maxHp = battlePokemon.effectedPokemon.maxHealth
if (damageDealt > 0 && maxHp > 0) {
val percent = Math.round(damageDealt.toFloat() / maxHp * 100F).coerceAtLeast(1)
battle.broadcastChatMessage(battleLang("damage_dealt", battlePokemon.getName(), "$percent%"))
}
if (effect != null) {
val lang = when (effect.id) {
@@ -222,4 +229,4 @@ class DamageInstruction(
}
}
}
}
@@ -35,6 +35,8 @@ class EndItemInstruction(val message: BattleMessage): InterpreterInstruction {
val battlePokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val itemEffect = message.effectAt(1) ?: return@dispatchWaiting
battlePokemon.heldItemManager.handleEndInstruction(battlePokemon, battle, message)
battlePokemon.revealedHeldItem = net.minecraft.world.item.ItemStack.EMPTY
battlePokemon.sendUpdate()
battle.minorBattleActions[battlePokemon.uuid] = message
battlePokemon.contextManager.remove(itemEffect.id, BattleContext.Type.ITEM)
if (message.hasOptionalArgument("eat")) {
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorTeamPacket
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
@@ -58,6 +60,11 @@ class InitializeInstruction(val instructionSet: InstructionSet, val message: Bat
battle.actors.forEach { actor ->
actor.sendUpdate(BattleSetTeamPokemonPacket(actor.pokemonList.map { it.effectedPokemon }))
battle.actors.forEach { viewer ->
viewer.sendUpdate(BattleActorTeamPacket(actor.uuid, actor.pokemonList.map {
it.toBattleDTO(ally = viewer == actor)
}))
}
val req = actor.request ?: return@forEach
actor.sendUpdate(BattleQueueRequestPacket(req))
}
@@ -68,8 +75,10 @@ class InitializeInstruction(val instructionSet: InstructionSet, val message: Bat
battle.dispatchGo {
battle.started = true
battle.activePokemon.mapNotNull { it.battlePokemon }.forEach { it.sendUpdate() }
battle.notifyAllOfUpdates()
battle.side1.playCries()
afterOnServer(seconds = 1.0F) { battle.side2.playCries() }
}
}
}
}
@@ -28,6 +28,8 @@ class InvertBoostInstruction(val message: BattleMessage): InterpreterInstruction
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
pokemon.statChanges.replaceAll { _, value -> -value }
pokemon.sendUpdate()
val name = pokemon.getName()
battle.broadcastChatMessage(battleLang("invertboost", name))
@@ -34,6 +34,8 @@ class ItemInstruction(val message: BattleMessage): InterpreterInstruction {
battle.dispatchWaiting(1.5F) {
val battlePokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
battlePokemon.heldItemManager.handleStartInstruction(battlePokemon, battle, message)
battlePokemon.revealedHeldItem = battlePokemon.effectedPokemon.heldItem.copy()
battlePokemon.sendUpdate()
battle.minorBattleActions[battlePokemon.uuid] = message
battlePokemon.contextManager.add(ShowdownInterpreter.getContextFromAction(message, BattleContext.Type.ITEM, battle))
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.net.messages.client.battle.MoveDTO
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
@@ -74,6 +76,14 @@ class MoveInstruction(
battle.dispatch { UntilDispatch { instructionSet.getMostRecentInstruction<MoveInstruction>(this)?.future?.isDone != false } }
battle.dispatch {
val knownMove = userPokemon.revealedMoves.firstOrNull { it?.move == move.displayName }
if (knownMove != null) {
knownMove.timesUsed++
} else {
val slot = userPokemon.revealedMoves.indexOfFirst { it == null }
if (slot >= 0) userPokemon.revealedMoves[slot] = MoveDTO(move.displayName, 1)
}
userPokemon.sendUpdate()
val pokemonName = userPokemon.getName()
ShowdownInterpreter.lastCauser[battle.battleId] = message
// For Spread targets the message data only gives the pnx strings. So we can't know what pokemon are actually targeted until the previous messages have been interpreted
@@ -19,6 +19,13 @@ import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
*/
object PostUpdateInstruction : InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo { battle.actors.forEach { it.postUpdate() } }
battle.dispatchGo {
battle.actors.forEach { it.postUpdate() }
battle.notifyAllOfUpdates()
val viewers = battle.actors.flatMap { it.getPlayerUUIDs() } + battle.spectators
battle.actors.forEach { actor ->
actor.pokemonList.forEach { it.updateBattleActorInformation(viewers) }
}
}
}
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.pokemon.stats.Stats
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.PokemonBattle
@@ -27,6 +29,12 @@ class SetBoostInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchWaiting(1.5F) {
val pokemon = message.battlePokemon(0, battle) ?: return@dispatchWaiting
val stat = message.argumentAt(1)?.let { Stats.getStat(it) }
val amount = message.argumentAt(2)?.toIntOrNull()
if (stat != null && amount != null) {
pokemon.statChanges[stat] = amount.coerceIn(-6, 6)
pokemon.sendUpdate()
}
val pokemonName = pokemon.getName()
val effectID = message.effect()?.id ?: return@dispatchWaiting
val lang = battleLang("setboost.$effectID", pokemonName)
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.pokemon.stats.Stats
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.PokemonBattle
@@ -29,6 +31,15 @@ class SwapBoostInstruction(val message: BattleMessage): InterpreterInstruction {
val pokemonName = pokemon.getName()
val targetPokemon = message.battlePokemon(1, battle) ?: return@dispatchWaiting
val targetPokemonName = targetPokemon.getName()
val stats = message.argumentAt(2)?.split(',')?.map { Stats.getStat(it.trim()) }
?: (pokemon.statChanges.keys + targetPokemon.statChanges.keys).toList()
stats.forEach { stat ->
val previous = pokemon.statChanges[stat] ?: 0
pokemon.statChanges[stat] = targetPokemon.statChanges[stat] ?: 0
targetPokemon.statChanges[stat] = previous
}
pokemon.sendUpdate()
targetPokemon.sendUpdate()
val effectID = message.effect()?.id ?: return@dispatchWaiting
val lang = when (effectID) {
"guardswap", "powerswap", "heartswap" -> battleLang("swapboost.$effectID", pokemonName)
@@ -129,6 +129,14 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
activePokemon.battlePokemon?.let { oldPokemon ->
if (publicMessage.effect()?.id == "batonpass") oldPokemon.contextManager.swap(pokemon.contextManager, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
pokemon.statChanges.clear()
if (publicMessage.effect()?.id == "batonpass") pokemon.statChanges.putAll(oldPokemon.statChanges)
oldPokemon.statChanges.clear()
if (oldPokemon.transformed != null) {
oldPokemon.revealedMoves.indices.forEach { oldPokemon.revealedMoves[it] = null }
oldPokemon.revealedAbility = null
}
oldPokemon.transformed = null
oldPokemon.contextManager.clear(BattleContext.Type.VOLATILE, BattleContext.Type.BOOST, BattleContext.Type.UNBOOST)
battle.majorBattleActions[oldPokemon.uuid] = publicMessage
@@ -215,6 +223,8 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
}
private fun broadcastSwitch(battle: PokemonBattle, actor: BattleActor, newPokemon: BattlePokemon, illusion: BattlePokemon?) {
// The active slot and its illusion are now set, so reveal the identity that entered battle.
newPokemon.sendUpdate()
val publicPokemon = (illusion ?: newPokemon).effectedPokemon
val publicLang = publicPokemon.nickname?.let { nickname ->
battleLang("switch.other.nickname", actor.getName(), nickname, publicPokemon.species.translatedName)
@@ -229,4 +239,4 @@ class SwitchInstruction(val instructionSet: InstructionSet, val battleActor: Bat
}
}
}
}
@@ -30,6 +30,8 @@ class TerastallizeInstruction(val message: BattleMessage): InterpreterInstructio
val battlePokemon = message.battlePokemon(0, battle) ?: return
val type = message.effectAt(1)?.let { TeraTypes.get(it.id) } ?: return
battle.dispatchWaiting {
battlePokemon.terastallized = type
battlePokemon.sendUpdate()
val pokemonName = battlePokemon.getName()
battle.broadcastChatMessage(battleLang("terastallize", pokemonName, type.displayName).yellow())
CobblemonEvents.TERASTALLIZATION.post(TerastallizationEvent(battle, battlePokemon, type))
@@ -46,6 +46,16 @@ class TransformInstruction(val battle: PokemonBattle, val message: BattleMessage
}
battle.dispatchWaiting {
pokemon.transformed = targetPokemon
pokemon.statChanges.clear()
pokemon.statChanges.putAll(targetPokemon.statChanges)
for (index in pokemon.revealedMoves.indices) {
pokemon.revealedMoves[index] = targetPokemon.moveSet.get(index)?.let {
com.cobblemon.mod.common.net.messages.client.battle.MoveDTO(it.displayName, 0)
}
}
pokemon.revealedAbility = targetPokemon.effectedPokemon.ability.name
pokemon.sendUpdate()
val mock = pokemon.entity?.effects?.mockEffect?.mock
val pokemonName = pokemon.getName()
val targetPokemonName = targetPokemon.getName()
@@ -63,4 +73,4 @@ class TransformInstruction(val battle: PokemonBattle, val message: BattleMessage
battle.minorBattleActions[pokemon.uuid] = message
}
}
}
}
@@ -8,6 +8,24 @@
package com.cobblemon.mod.common.battles.pokemon
import net.minecraft.world.item.ItemStack
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.net.messages.client.battle.MoveDTO
import com.cobblemon.mod.common.net.messages.client.battle.BattlePokemonDTO
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorInformationPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleInitializePacket
import com.cobblemon.mod.common.api.types.tera.TeraType
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
@@ -120,6 +138,13 @@ open class BattlePokemon(
val contextManager = ContextManager()
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,8 +154,72 @@ open class BattlePokemon(
}
}
fun toBattleDTO(ally: Boolean, forceReveal: Boolean = false): BattlePokemonDTO {
val moves = (transformed?.effectedPokemon ?: effectedPokemon).moveSet.getMovesWithNulls().map { move ->
move?.let { MoveDTO(it.displayName, revealedMoves.firstOrNull { known -> known?.move == it.displayName }?.timesUsed ?: 0) }
}
val boostMultipliers = statChanges.entries.mapNotNull { (stat, stage) ->
val standardStat = stat as? Stats ?: return@mapNotNull null
val base = if (standardStat == Stats.ACCURACY || standardStat == Stats.EVASION) 3.0 else 2.0
val clamped = stage.coerceIn(-6, 6)
standardStat to if (clamped >= 0) (base + clamped) / base else base / (base - clamped)
}.toMap().filterValues { it != 1.0 }
val format = if (::actor.isInitialized && actor.isInitialized()) actor.battle.format else null
val isTeamSheet = format?.isOpenTeamSheet == true
if (ally || isTeamSheet) {
val allyDto = BattleInitializePacket.ActiveBattlePokemonDTO.fromPokemon(this, ally, getIllusion())
return BattlePokemonDTO(
this.effectedPokemon.uuid,
this.effectedPokemon.isFainted(),
(transformed?.effectedPokemon ?: effectedPokemon).ability.name,
moves,
this.effectedPokemon.heldItem,
boostMultipliers,
if (ally) effectedPokemon.speed else null,
terastallized = terastallized,
allyDto
)
}
else {
// Team Preview reveals identities, while moves, abilities and items still require discovery.
val nonAllyDto = if (forceReveal || revealed || format?.hasTeamPreview == true) {
BattleInitializePacket.ActiveBattlePokemonDTO.fromPokemon(this, false, getIllusion())
} else null
return BattlePokemonDTO(
this.effectedPokemon.uuid,
this.effectedPokemon.isFainted(),
revealedAbility,
revealedMoves,
revealedHeldItem,
boostMultipliers,
speed = null,
terastallized = terastallized,
nonAllyDto
)
}
}
fun updateBattleActorInformation(uuids: List<UUID>) {
uuids.forEach {
if (actor.uuid == it) {
it.getPlayer()?.sendPacket(
BattleActorInformationPacket(actor.uuid, toBattleDTO(true))
)
}
else {
it.getPlayer()?.sendPacket(
BattleActorInformationPacket(actor.uuid, toBattleDTO(false))
)
}
}
}
fun sendUpdate() {
actor.sendUpdate(BattleUpdateTeamPokemonPacket(effectedPokemon))
revealed = revealed || isSentOut()
updateBattleActorInformation(actor.battle.actors.flatMap { it.getPlayerUUIDs() } + actor.battle.spectators)
}
fun isSentOut() = actor.battle.activePokemon.any { it.battlePokemon == this }
@@ -141,5 +230,7 @@ open class BattlePokemon(
!isSentOut() && !willBeSwitchedIn && health > 0
}
fun getIllusion(): BattlePokemon? = this.actor.activePokemon.find { it.battlePokemon == this }?.illusion
}
fun getIllusion(): BattlePokemon? = if (::actor.isInitialized) {
actor.activePokemon.find { it.battlePokemon == this }?.illusion
} else null
}
@@ -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 startSelection()
fun endSelection()
fun tick()
fun mustChooseBy(): Instant
fun timeout() {
actor.setActionResponses(listOf(ForfeitActionResponse()))
}
}
@@ -0,0 +1,45 @@
/*
* 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 ShowdownTimer(override val battle: PokemonBattle, override val actor: PlayerBattleActor): PlayerBattleTimer {
private lateinit var turnStart: Instant
private var secondsRemaining = 210
private var hasSelected = true
override fun tick() {
if (hasSelected || !::turnStart.isInitialized || battle.ended) return
val secondsSinceStart = Duration.between(turnStart, Instant.now()).seconds
if (secondsRemaining - secondsSinceStart <= 0) {
hasSelected = true
timeout()
}
}
override fun startSelection() {
turnStart = Instant.now()
hasSelected = false
}
override fun endSelection() {
if (hasSelected || !::turnStart.isInitialized) return
hasSelected = true
val secondsSinceStart = Duration.between(turnStart, Instant.now()).seconds.toInt()
secondsRemaining = secondsRemaining - secondsSinceStart + 10
}
override fun mustChooseBy(): Instant {
return turnStart.plusSeconds(secondsRemaining.toLong())
}
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.client
import com.cobblemon.mod.common.client.battle.ClientBattleInformationRepository
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonBlocks
@@ -124,6 +126,7 @@ object CobblemonClient {
storage.onLogout()
battle = null
battleOverlay.onLogout()
ClientBattleInformationRepository.clear()
ClientTaskTracker.clear()
checkedStarterScreen = false
overlay.resetAttachedToast()
@@ -483,6 +486,7 @@ object CobblemonClient {
}
fun endBattle() {
ClientBattleInformationRepository.clear()
battle = null
battleOverlay.lastKnownBattle = null
BattleMusicController.endMusic()
@@ -84,6 +84,7 @@ class ClientBattle(
pendingActionRequests.map { it.response!! }
)
)
ClientBattleInformationRepository.mustChooseBy = null
mustChoose = false
}
}
@@ -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.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
fun clear() {
actors.clear()
battles.clear()
actorActualSides.clear()
mustChooseBy = null
}
}
@@ -9,6 +9,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
@@ -21,6 +22,21 @@ import net.minecraft.util.FormattedCharSequence
class ClientBattleMessageQueue {
var listeners = mutableListOf<(FormattedCharSequence) -> Unit>()
private val messages = mutableListOf<FormattedCharSequence>()
private val components = mutableListOf<Component>()
private val componentListeners = mutableListOf<(Component) -> Unit>()
fun addComponents(messages: Iterable<Component>) {
messages.forEach { message ->
components.add(message)
componentListeners.toList().forEach { it(message) }
}
}
fun subscribeComponents(listener: (Component) -> Unit): () -> Unit {
componentListeners.add(listener)
components.forEach(listener)
return { componentListeners.remove(listener) }
}
fun add(messages: Iterable<FormattedCharSequence>) {
this.messages.addAll(messages)
@@ -31,4 +47,4 @@ class ClientBattleMessageQueue {
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,42 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.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) {
if (pokemon !in team || pokemon in selection || selection.size >= selections) return
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()))
}
}
@@ -20,10 +20,12 @@ import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleGeneralActionS
import com.cobblemon.mod.common.client.gui.battle.subscreen.BattleSwitchPokemonSelection
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,15 +46,16 @@ 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)
var queuedActions = mutableListOf<() -> Unit>()
override fun init() {
super.init()
if (this::messagePane.isInitialized) messagePane.dispose()
messagePane = BattleMessagePane(CobblemonClient.battle!!.messages)
messagePane.opacity = CobblemonClient.battleOverlay.opacityRatio.toFloat().coerceAtLeast(0.3F)
addRenderableWidget(messagePane)
@@ -121,16 +124,17 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
}
val currentSelection = getCurrentActionSelection()
if (currentSelection == null || currentSelection is BattleGeneralActionSelection ) {
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),
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 +148,11 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
queuedActions.forEach { it() }
queuedActions.clear()
CobblemonClient.battleOverlay.mouseX = mouseX
CobblemonClient.battleOverlay.mouseY = mouseY
// The benched-team portrait columns + active hover panels are queued during the HUD pass and drawn
// here, last, so they sit above the switch-menu underlay, our own widgets and the chat.
CobblemonClient.battleOverlay.renderDeferredColumns(context)
}
override fun renderBackground(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
@@ -151,8 +160,6 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
}
fun deriveRootActionSelection(actor: ClientBattleActor, request: SingleActionRequest): BattleActionSelection? {
return if (request.forceSwitch) {
BattleSwitchPokemonSelection(this, request)
} else {
@@ -175,11 +182,23 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
CobblemonClient.battle?.minimised = true
}
override fun removed() {
if (this::messagePane.isInitialized) messagePane.dispose()
super.removed()
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (this::messagePane.isInitialized) messagePane.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
if (CobblemonClient.battleOverlay.columnMouseDragged(mouseX, mouseY)) return true
if (this::messagePane.isInitialized && messagePane.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (this::messagePane.isInitialized) messagePane.mouseReleased(mouseX, mouseY, button)
if (CobblemonClient.battleOverlay.columnMouseReleased()) return true
return super.mouseReleased(mouseX, mouseY, button)
}
override fun charTyped(chr: Char, modifiers: Int): Boolean {
if (chr.toString().equals(PartySendBinding.boundKey().displayName.string, ignoreCase = true) && CobblemonClient.battleOverlay.opacity == BattleOverlay.MAX_OPACITY && PartySendBinding.canAction()) {
return minimizeBattle()
@@ -188,14 +207,18 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
// Grab a benched-team portrait column to drag it (left-click). Consumes the click if one was hit.
if (button == 0 && CobblemonClient.battleOverlay.columnMouseClicked(mouseX, mouseY)) return true
if (button == PartySendBinding.boundKey().value && CobblemonClient.battleOverlay.opacity == BattleOverlay.MAX_OPACITY && PartySendBinding.canAction()) {
return minimizeBattle()
}
val battle = CobblemonClient.battle
if (battle?.spectating == true && specBackButton.isHovered(mouseX, mouseY)) {
RemoveSpectatorPacket(battle.battleId).sendToServer()
CobblemonClient.endBattle()
if (battle?.spectating == true) {
if (specBackButton.isHovered(mouseX, mouseY)) {
RemoveSpectatorPacket(battle.battleId).sendToServer()
CobblemonClient.endBattle()
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
@@ -206,4 +229,4 @@ class BattleGUI : Screen(battleLang("gui.title")), CobblemonRenderable {
PartySendBinding.actioned()
return true
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,682 @@
/*
* 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
import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.text.bold
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.battle.ClientBattlePokemon
import com.cobblemon.mod.common.client.gui.TypeIcon
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.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.asTranslated
import com.cobblemon.mod.common.net.messages.client.battle.BattlePokemonDTO
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.TextColor
import net.minecraft.network.chat.contents.TranslatableContents
import java.text.DecimalFormat
import kotlin.math.ceil
import kotlin.math.floor
/**
* The "Info" style expanded stat panel for a single battle Pokémon (form, buffs/debuffs, ability,
* moves, speed/speed-tier, types, held item). It renders to the *side* of a tile, with a connector
* pointing back at it, and expands inwards (towards the screen centre) on both sides.
*
* Shared by the team Info screen (the team Info screen)
* and the on-field benched-team portrait column ([BattleOverlay.drawPartyColumn]) so the two stay identical.
*
* Knowledge gating is inherited from [dto]: the team owner gets full data while opponents only carry
* what has been revealed, so unrevealed ability/moves show "?" and speed falls back to a tier range.
*/
object BattlePokemonInfoPanel {
private val expandedPokemonInfo = cobblemonResource("textures/gui/battle/expanded_pokemon_info_center.png")
// Variant for non-active Pokémon: the left (Buffs/Debuffs) column is cleared for a Current HP gauge.
private val expandedPokemonInfoInactive = cobblemonResource("textures/gui/battle/expanded_pokemon_info_center_inactive.png")
// Same battle health-bar texture the active Pokémon's tile uses, for the Current HP gauge.
private val healthBar = cobblemonResource("textures/gui/battle/health_bar.png")
private val hoverGap = cobblemonResource("textures/gui/battle/hover_gap.png")
private val hoverGapReversed = cobblemonResource("textures/gui/battle/hover_gap_reversed.png")
private val multiplierFormat = DecimalFormat("0.##")
// Boost value slots span x=46..65; leave one pixel of padding at the battle text scale.
internal const val BOOST_VALUE_CENTER_X = 55.5
internal const val BOOST_VALUE_MAX_WIDTH = 36 // 18 GUI pixels at BattleOverlay.SCALE.
fun renderUnknownTile(context: GuiGraphics, x: Float, y: Float, reversed: Boolean, scale: Float = 1F, opacity: Number = 1F) {
val matrices = context.pose()
val insetX = if (reversed) 8 else 5
matrices.pushPose()
matrices.translate(x.toDouble(), y.toDouble(), 0.0)
matrices.scale(scale, scale, scale)
blitk(
matrixStack = matrices,
texture = BattleOverlay.battleInfoUnderlay,
x = insetX,
y = 4,
width = 28,
height = 28,
alpha = opacity
)
// Draw the complete 8x10 asset at twice its size, centred in the portrait window.
blitk(
matrixStack = matrices,
texture = BattleOverlay.questionMarkIcon,
x = (insetX + 6) / 2F,
y = 4,
width = 8,
height = 10,
scale = 2F,
alpha = opacity
)
blitk(
matrixStack = matrices,
texture = if (reversed) BattleOverlay.partyColumnTileReversed else BattleOverlay.partyColumnTile,
x = 0,
y = 0,
width = 41,
height = 39,
alpha = opacity
)
matrices.popPose()
}
fun render(
context: GuiGraphics,
tileX: Float,
tileY: Float,
tileWidth: Float,
reversed: Boolean,
pokemon: ClientBattlePokemon,
dto: BattlePokemonDTO,
showGap: Boolean = true,
showBuffs: Boolean = true
) {
context.pose().pushPose()
context.pose().translate(0.0, 0.0, 300.0)
// Panel sits just beside the tile (inwards) with the connector bridging the gap.
val startX = if (reversed) tileX - 136 else tileX + tileWidth - 1
val startY = tileY
val species = pokemon.species
val form = species.getForm(pokemon.state.currentAspects)
// The connector is only shown for the Info screen tiles, not the on-field draggable column.
if (showGap) {
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 = if (showBuffs) expandedPokemonInfo else expandedPokemonInfoInactive,
x = startX,
y = startY,
height = 112,
width = 137,
textureHeight = 112,
textureWidth = 137,
)
drawScaledText(
context = context,
text = "deltaclient.ui.battle.label.pokemon_form".asTranslated().bold(),
x = startX + 36,
y = startY + 6.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = getFormText(species, form),
x = startX + 36,
y = startY + 13.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
if (showBuffs) {
drawScaledText(
context = context,
text = "deltaclient.ui.battle.label.buffs".asTranslated().bold(),
x = startX + 36,
y = startY + 23.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = "cobblemon.stat.attack.name".asTranslated().statColored(Stats.ATTACK),
x = startX + 10,
y = startY + 31.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = false
)
dto.buffs[Stats.ATTACK]?.let {
drawScaledText(
context = context,
text = getBoostText(Stats.ATTACK, it),
x = startX + BOOST_VALUE_CENTER_X,
y = startY + 31.5,
scale = BattleOverlay.SCALE,
maxCharacterWidth = BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.defence.name".asTranslated().statColored(Stats.DEFENCE),
x = startX + 10,
y = startY + 39.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = false
)
dto.buffs[Stats.DEFENCE]?.let {
drawScaledText(
context = context,
text = getBoostText(Stats.DEFENCE, it),
x = startX + BOOST_VALUE_CENTER_X,
y = startY + 39.5,
scale = BattleOverlay.SCALE,
maxCharacterWidth = BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "deltaclient.ui.battle.stats.special_attack".asTranslated().statColored(Stats.SPECIAL_ATTACK),
x = startX + 10,
y = startY + 47.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = false
)
dto.buffs[Stats.SPECIAL_ATTACK]?.let {
drawScaledText(
context = context,
text = getBoostText(Stats.SPECIAL_ATTACK, it),
x = startX + BOOST_VALUE_CENTER_X,
y = startY + 47.5,
scale = BattleOverlay.SCALE,
maxCharacterWidth = BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "deltaclient.ui.battle.stats.special_defence".asTranslated().statColored(Stats.SPECIAL_DEFENCE),
x = startX + 10,
y = startY + 55.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = false
)
dto.buffs[Stats.SPECIAL_DEFENCE]?.let {
drawScaledText(
context = context,
text = getBoostText(Stats.SPECIAL_DEFENCE, it),
x = startX + BOOST_VALUE_CENTER_X,
y = startY + 55.5,
scale = BattleOverlay.SCALE,
maxCharacterWidth = BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.speed.name".asTranslated().statColored(Stats.SPEED),
x = startX + 10,
y = startY + 63.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = false
)
dto.buffs[Stats.SPEED]?.let {
drawScaledText(
context = context,
text = getBoostText(Stats.SPEED, it),
x = startX + BOOST_VALUE_CENTER_X,
y = startY + 63.5,
scale = BattleOverlay.SCALE,
maxCharacterWidth = BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.accuracy.name".asTranslated(),
x = startX + 10,
y = startY + 71.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = false
)
dto.buffs[Stats.ACCURACY]?.let {
drawScaledText(
context = context,
text = getBoostText(Stats.ACCURACY, it),
x = startX + BOOST_VALUE_CENTER_X,
y = startY + 71.5,
scale = BattleOverlay.SCALE,
maxCharacterWidth = BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "cobblemon.stat.evasion.name".asTranslated(),
x = startX + 10,
y = startY + 79.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = false
)
dto.buffs[Stats.EVASION]?.let {
drawScaledText(
context = context,
text = getBoostText(Stats.EVASION, it),
x = startX + BOOST_VALUE_CENTER_X,
y = startY + 79.5,
scale = BattleOverlay.SCALE,
maxCharacterWidth = BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
} else {
// Non-active Pokémon have no stat stages; the inactive panel asset already clears the
// Buffs/Debuffs column, so we just draw a "Current HP" gauge on that clean area.
drawScaledText(
context = context,
text = "deltaclient.ui.battle.label.current_hp".asTranslated().bold(),
x = startX + 36,
y = startY + 23.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
// Owner sees flat HP (hp/maxHp); opponents carry a 0..1 ratio.
val hpRatio = (if (pokemon.isHpFlat) pokemon.hpValue / pokemon.maxHp.toInt() else pokemon.hpValue)
.coerceIn(0f, 1f)
val (hpRed, hpGreen) = getDepletableRedGreen(hpRatio)
// Same battle health-bar texture + tint the active Pokémon uses, sized to fill the asset's
// bar slot beneath the "Current HP" label (slot ≈ x8..64, y30..36).
val barX = startX + 8
val barW = hpRatio * 57f
blitk(
matrixStack = context.pose(),
texture = healthBar,
x = barX,
y = startY + 30,
width = barW,
height = 6,
textureWidth = 99,
textureHeight = 6,
red = hpRed * 0.8f,
green = hpGreen * 0.8f,
blue = 0.27f
)
// The texture's right end is angled, so a left-clipped fill has a border on every side except
// the right. Re-draw the texture's square left-edge column (texel 0) at the fill's right edge
// to close the border into a full rectangle.
if (barW >= 1f) {
blitk(
matrixStack = context.pose(),
texture = healthBar,
x = barX + barW - 1f,
y = startY + 30,
width = 1f,
height = 6,
textureWidth = 99,
textureHeight = 6,
red = hpRed * 0.8f,
green = hpGreen * 0.8f,
blue = 0.27f
)
}
// Percentage centred inside the bar — and exact values for your own Pokémon — exactly like
// the active Pokémon's health bar does in battle.
val hpText = if (pokemon.isHpFlat) {
"${pokemon.hpValue.toInt()}/${pokemon.maxHp.toInt()} (${ceil(hpRatio * 100).toInt()}%)"
} else {
"${ceil(hpRatio * 100).toInt()}%"
}
drawScaledText(
context = context,
text = hpText.text(),
x = startX + 36,
y = startY + 31.0,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "deltaclient.ui.battle.label.ability".asTranslated().bold(),
x = startX + 100,
y = startY + 6.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
val ability = dto.ability?.let { Abilities.get(it) }
val abilityName = ability?.displayName?.asTranslated() ?: "?".text()
drawScaledText(
context = context,
text = abilityName,
x = startX + 100,
y = startY + 13.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = "deltaclient.ui.battle.label.moves".asTranslated().bold(),
x = startX + 100,
y = startY + 23.5,
scale = BattleOverlay.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.SCALE,
shadow = true,
centered = true
)
}
}
else {
val questionMarkText = "?".text()
dto.moves.forEachIndexed { index, moveDTO ->
val move = moveDTO?.move ?: questionMarkText
val moveColor = moveDTO?.move?.let { typeColorFor(it) }
drawScaledText(
context = context,
text = move.string.text().let { t -> if (moveColor != null) t.colored(moveColor) else t },
x = if (move == questionMarkText) startX + 100 else startX + 74,
y = startY + 31.5 + index * 8,
scale = BattleOverlay.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.SCALE,
shadow = true,
)
}
}
}
val dtoSpeed = dto.speed
if (dtoSpeed != null) {
val speedBoost = dto.buffs[Stats.SPEED] ?: 1.0
val speedAfterBoost = (dtoSpeed * speedBoost).toInt()
drawScaledText(
context = context,
text = "cobblemon.stat.speed.name".asTranslated(),
x = startX + 100,
y = startY + 65.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
drawScaledText(
context = context,
text = speedAfterBoost.toString().text(),
x = startX + 100,
y = startY + 72.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
}
else {
drawScaledText(
context = context,
text = "deltaclient.ui.battle.label.speed_tier".asTranslated(),
x = startX + 100,
y = startY + 65.5,
scale = BattleOverlay.SCALE,
shadow = true,
centered = true
)
val speedBoost = dto.buffs[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.SCALE,
shadow = true,
centered = true
)
}
drawScaledText(
context = context,
text = "deltaclient.ui.battle.label.held_item".asTranslated(),
x = startX + 40,
y = startY + 99.5,
scale = BattleOverlay.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 = BattleOverlay.questionMarkIcon,
x = startX + 6,
y = startY + 92.5,
height = 11,
width = 10,
)
}
context.pose().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 "deltaclient.ui.battle.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()
}
internal fun getBoostText(stat: Stats, multiplier: Double): MutableComponent {
val stage = stageFromMultiplier(stat, multiplier)
val sign = if (stage > 0) "+" else ""
val str = "$sign$stage (${multiplierFormat.format(multiplier)}x)"
return when {
multiplier > 1.0 -> str.green()
multiplier < 1.0 -> str.red()
else -> str.text()
}
}
// Converts a stat-boost multiplier back to its stage. Accuracy/Evasion use a /3 base, others /2.
private fun stageFromMultiplier(stat: Stats, multiplier: Double): Int {
val base = if (stat == Stats.ACCURACY || stat == Stats.EVASION) 3.0 else 2.0
return if (multiplier >= 1.0) Math.round(multiplier * base - base).toInt()
else Math.round(base - base / multiplier).toInt()
}
// The type colour for a move from its "cobblemon.move.<name>" display name, or null if unknown.
internal fun typeColorFor(move: Component): Int? {
val key = (move.contents as? TranslatableContents)?.key ?: return null
if (!key.startsWith("cobblemon.move.")) return null
val type = Moves.getByName(key.removePrefix("cobblemon.move."))?.elementalType?.name ?: return null
return typeColor(type)
}
private fun typeColor(type: String): Int = when (type.lowercase()) {
"bug" -> 0xA8B820
"dark" -> 0x705848
"dragon" -> 0x7038F8
"electric" -> 0xF8D030
"fairy" -> 0xEE99AC
"fighting" -> 0xC03028
"fire" -> 0xF08030
"flying" -> 0xA890F0
"ghost" -> 0x705898
"grass" -> 0x78C850
"ground" -> 0xE0C068
"ice" -> 0x98D8D8
"normal" -> 0xA8A878
"poison" -> 0xA040A0
"psychic" -> 0xF85888
"rock" -> 0xB8A038
"steel" -> 0xB8B8D0
"water" -> 0x6890F0
else -> 0xFFFFFF
}
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()
}
}
internal fun MutableComponent.colored(rgb: Int): MutableComponent = this.withStyle { it.withColor(TextColor.fromRgb(rgb)) }
// Buffs/Debuffs stat names are all white. (Per-stat colours can be re-enabled by returning them here.)
internal fun statColor(stat: Stats): Int? = null
internal fun MutableComponent.statColored(stat: Stats): MutableComponent {
val rgb = statColor(stat) ?: return this
return this.colored(rgb)
}
@@ -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".asTranslated().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?.asTranslated() ?: "?".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()
}
}
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.client.gui.battle.subscreen
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
@@ -74,6 +76,12 @@ class BattleGeneralActionSelection(
battleGUI.changeActionSelection(ForfeitConfirmationSelection(battleGUI, request))
playDownSound(Minecraft.getInstance().soundManager)
}
if (battle.side2.actors.all { it.type == ActorType.PLAYER }) {
addOption(rank++, battleLang("ui.team_info.button"), cobblemonResource("textures/gui/battle/team_info.png")) {
battleGUI.changeActionSelection(BattleTeamInfoSelection(battleGUI, request))
playDownSound(Minecraft.getInstance().soundManager)
}
}
}
}
}
@@ -121,4 +129,4 @@ class BattleGeneralActionSelection(
}
override fun narrationPriority() = NarratableEntry.NarrationPriority.NONE
}
}
@@ -10,27 +10,41 @@ 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,
@@ -51,7 +65,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 +102,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 +166,7 @@ class BattleMoveSelection(
// Move Category
MoveCategoryIcon(
x = x + 48,
x = x + 41,
y = y + 14.5,
category = moveTemplate.damageCategory,
opacity = moveSelection.opacity
@@ -168,14 +196,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 = moveTemplate
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 +483,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
@@ -224,4 +511,4 @@ class BattleMoveSelection(
override fun playDownSound(soundManager: SoundManager) {
soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.GUI_CLICK, 1.0F))
}
}
}
@@ -0,0 +1,880 @@
/*
* 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.client.battle.SingleActionRequest
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.BattlePokemonInfoPanel
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.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,
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),
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 ->
val (slotX, slotY) = getSlotPosition(index, 1)
teamTiles.add(PokemonTile(slotX, slotY, pokemon, this, false, actorToPokemon.first().first))
}
actorToPokemon.last().second?.forEachIndexed { index, pokemon ->
val (slotX, slotY) = getSlotPosition(index, 2)
opponentTiles.add(PokemonTile(slotX, slotY, pokemon, this, true, actorToPokemon.last().first))
}
}
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.pokemonDto.activeBattlePokemonDTO != null && 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".asTranslated().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,
private val initialPokemonDto: BattlePokemonDTO,
val widget: BattleTeamInfoSelection,
val reversed: Boolean,
val actor: ClientBattleActor
) {
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()
val pokemonDto: BattlePokemonDTO
get() = ClientBattleInformationRepository.actors[actor.uuid]?.firstOrNull { it.uuid == initialPokemonDto.uuid } ?: initialPokemonDto
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
if (pokemon == null) {
BattlePokemonInfoPanel.renderUnknownTile(context, x, y, reversed)
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 = BattlePokemonInfoPanel.getBoostText(Stats.ATTACK, it),
x = startX + BattlePokemonInfoPanel.BOOST_VALUE_CENTER_X,
y = startY + 31.5,
scale = BattleOverlay.Companion.SCALE,
maxCharacterWidth = BattlePokemonInfoPanel.BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
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 = BattlePokemonInfoPanel.getBoostText(Stats.DEFENCE, it),
x = startX + BattlePokemonInfoPanel.BOOST_VALUE_CENTER_X,
y = startY + 39.5,
scale = BattleOverlay.Companion.SCALE,
maxCharacterWidth = BattlePokemonInfoPanel.BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
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 = BattlePokemonInfoPanel.getBoostText(Stats.SPECIAL_ATTACK, it),
x = startX + BattlePokemonInfoPanel.BOOST_VALUE_CENTER_X,
y = startY + 47.5,
scale = BattleOverlay.Companion.SCALE,
maxCharacterWidth = BattlePokemonInfoPanel.BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
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 = BattlePokemonInfoPanel.getBoostText(Stats.SPECIAL_DEFENCE, it),
x = startX + BattlePokemonInfoPanel.BOOST_VALUE_CENTER_X,
y = startY + 55.5,
scale = BattleOverlay.Companion.SCALE,
maxCharacterWidth = BattlePokemonInfoPanel.BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
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 = BattlePokemonInfoPanel.getBoostText(Stats.SPEED, it),
x = startX + BattlePokemonInfoPanel.BOOST_VALUE_CENTER_X,
y = startY + 63.5,
scale = BattleOverlay.Companion.SCALE,
maxCharacterWidth = BattlePokemonInfoPanel.BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
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 = BattlePokemonInfoPanel.getBoostText(Stats.ACCURACY, it),
x = startX + BattlePokemonInfoPanel.BOOST_VALUE_CENTER_X,
y = startY + 71.5,
scale = BattleOverlay.Companion.SCALE,
maxCharacterWidth = BattlePokemonInfoPanel.BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
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 = BattlePokemonInfoPanel.getBoostText(Stats.EVASION, it),
x = startX + BattlePokemonInfoPanel.BOOST_VALUE_CENTER_X,
y = startY + 79.5,
scale = BattleOverlay.Companion.SCALE,
maxCharacterWidth = BattlePokemonInfoPanel.BOOST_VALUE_MAX_WIDTH,
shadow = true,
centered = true
)
}
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?.asTranslated() ?: "?".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()
}
}
}
@@ -10,6 +10,7 @@ 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.render.drawScaledText
@@ -17,7 +18,15 @@ 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.network.chat.Style
import net.minecraft.network.chat.TextColor
import net.minecraft.network.chat.contents.TranslatableContents
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.
@@ -36,29 +45,80 @@ class BattleMessagePane(
), CobblemonRenderable {
var opacity = 1F
private var scrolling = false
private var resizing = false
private var movingBox = false
// Offset from the mouse to the box's top-left when a move-drag begins, so the box tracks the cursor
// exactly (positioning absolutely avoids the GUI-scale mismatch that accumulating deltas causes).
private var dragOffsetX = 0.0
private var dragOffsetY = 0.0
val appropriateX: Int
get() = minecraft.window.guiScaledWidth - (FRAME_WIDTH + 12)
val appropriateY: Int
get() = minecraft.window.guiScaledHeight - (30 + (if (expanded) FRAME_EXPANDED_HEIGHT else FRAME_HEIGHT))
val battleMessages = mutableListOf<Component>()
private val unsubscribe: () -> Unit
fun dispose() = unsubscribe()
init {
correctSize()
//setRenderBackground(false)
messageQueue.subscribe {
val fullyScrolledDown = maxScroll - scrollAmount < 10
addEntry(BattleMessageLine(this, it))
if (fullyScrolledDown) {
scrollAmount = maxScroll.toDouble()
}
scrollAmount = maxScroll.toDouble()
unsubscribe = messageQueue.subscribeComponents {
battleMessages.add(it)
correctBattleText()
}
}
private fun correctBattleText() {
val isFullyScrolled = maxScroll - scrollAmount < 10
clearEntries()
val textRenderer = Minecraft.getInstance().font
for (message in battleMessages) {
// Turn announcements ("It is now turn N") render as a centered "Turn N" divider header.
val contents = message.contents
if (contents is TranslatableContents && contents.key == "cobblemon.battle.turn") {
val turnNum = contents.args.firstOrNull()?.toString() ?: ""
addEntry(BattleMessageLine(this, null, Component.translatable("deltaclient.ui.battle.turn", turnNum).string))
continue
}
// Tint certain message categories by their translation key: weather/field/screen → teal,
// stat changes → purple, damage → blue. Other messages keep their existing styling.
var style = message.style.withBold(true).withFont(CobblemonResources.DEFAULT_LARGE)
val color = messageColor((contents as? TranslatableContents)?.key)
if (color != null) style = style.withColor(TextColor.fromRgb(color))
val line = message.copy().setStyle(style)
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()
}
}
/** Colour for a battle message based on its translation key, or null to keep its existing styling. */
private fun messageColor(key: String?): Int? = when {
key == null -> null
key == "cobblemon.battle.damage_dealt" -> 0x4298ED // damage % → blue
key.startsWith("cobblemon.battle.weather.") -> 0x42EDAF // weather → teal
key.startsWith("cobblemon.battle.fieldstart.") -> 0x42EDAF // terrains, rooms, gravity, etc.
key.startsWith("cobblemon.battle.fieldend.") -> 0x42EDAF
key.startsWith("cobblemon.battle.fieldactivate.") -> 0x42EDAF
key.startsWith("cobblemon.battle.sidestart.") -> 0x42EDAF // Reflect, Light Screen, etc.
key.startsWith("cobblemon.battle.sideend.") -> 0x42EDAF
key.startsWith("cobblemon.battle.boost.") -> 0x8C42ED // stat rose → purple
key.startsWith("cobblemon.battle.unboost.") -> 0x8C42ED // stat fell → purple
else -> null
}
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 +131,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 +184,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 +204,7 @@ class BattleMessagePane(
x = x + 6,
y = top - 2,
height = 1,
width = LINE_WIDTH,
width = battleLogWidth - 18,
alpha = opacity
)
@@ -112,14 +214,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,62 +230,198 @@ 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) {
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
}
if (button != 0) return false
// Intentionally NOT calling super.mouseClicked — that selects the clicked log line, which we no
// longer want. Instead the press grabs the scrollbar, the bottom-right resize handle, or (anywhere
// else on the box) moves the whole box.
updateScrollingState(mouseX, mouseY)
if (scrolling) {
focused = getEntryAtPosition(mouseX, mouseY)
isDragging = true
if (scrolling) return true
if (isOnResizeHandle(mouseX, mouseY)) {
resizing = true
return true
}
if (isWithinBox(mouseX, mouseY)) {
movingBox = true
dragOffsetX = mouseX - battleLogX
dragOffsetY = mouseY - battleLogY
return true
}
return false
}
return super.mouseClicked(mouseX, mouseY, button)
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
scrolling = false
resizing = false
movingBox = false
return super.mouseReleased(mouseX, mouseY, button)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (scrolling) {
if (mouseY < this.y) {
scrollAmount = 0.0
} else if (mouseY > bottom) {
scrollAmount = maxScroll.toDouble()
} else {
scrollAmount += deltaY
when {
mouseY < this.y -> scrollAmount = 0.0
mouseY > bottom -> scrollAmount = maxScroll.toDouble()
else -> scrollAmount += deltaY
}
return true
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
if (resizing) {
battleLogHeight = max(mouseY.toInt() + 7 - y, TEXT_BOX_HEIGHT)
battleLogWidth = max(mouseX.toInt() - x, TEXT_BOX_WIDTH)
correctSize()
correctBattleText()
return true
}
if (movingBox) {
battleLogX = mouseX - dragOffsetX
battleLogY = mouseY - dragOffsetY
return true
}
return false
}
/** The whole message box (frame included) — grab it anywhere here to move it. */
private fun isWithinBox(mouseX: Double, mouseY: Double): Boolean {
return mouseX >= battleLogX && mouseX <= battleLogX + battleLogWidth &&
mouseY >= battleLogY && mouseY <= battleLogY + battleLogHeight
}
/** The resize handle in the bottom-right corner (drag to expand); takes priority over moving. */
private fun isOnResizeHandle(mouseX: Double, mouseY: Double): Boolean {
return mouseX >= battleLogX + battleLogWidth - 13 && mouseX <= battleLogX + battleLogWidth &&
mouseY >= battleLogY + battleLogHeight - 10 && mouseY <= battleLogY + battleLogHeight
}
private fun updateScrollingState(mouseX: Double, mouseY: Double) {
@@ -193,7 +431,7 @@ class BattleMessagePane(
&& mouseY < bottom
}
class BattleMessageLine(val pane: BattleMessagePane, val line: FormattedCharSequence) : Entry<BattleMessageLine>() {
class BattleMessageLine(val pane: BattleMessagePane, val line: FormattedCharSequence?, val headerText: String? = null) : Entry<BattleMessageLine>() {
override fun getNarration() = "".text()
override fun render(
context: GuiGraphics,
@@ -207,13 +445,54 @@ class BattleMessagePane(
isHovered: Boolean,
partialTicks: Float
) {
if (headerText != null) {
// "Turn N" centered on the divider's true midpoint, with an equal-length line each side.
val comp = headerText.text().setStyle(Style.EMPTY.withBold(true).withFont(CobblemonResources.DEFAULT_LARGE))
val textWidth = Minecraft.getInstance().font.width(comp)
val dividerLeft = rowLeft - 3
val dividerRight = rowLeft + rowWidth - 7
val centerX = (dividerLeft + dividerRight) / 2
val lineY = rowTop + 2
val leftEnd = centerX - textWidth / 2 - 3
val rightStart = centerX + textWidth / 2 + 3
blitk(
matrixStack = context.pose(),
texture = CobblemonResources.WHITE,
x = dividerLeft,
y = lineY,
width = (leftEnd - dividerLeft).coerceAtLeast(0),
height = 1,
alpha = pane.opacity * 0.6f
)
blitk(
matrixStack = context.pose(),
texture = CobblemonResources.WHITE,
x = rightStart,
y = lineY,
width = (dividerRight - rightStart).coerceAtLeast(0),
height = 1,
alpha = pane.opacity * 0.6f
)
drawScaledText(
context = context,
text = comp,
x = centerX - textWidth / 2,
y = rowTop - 2,
opacity = pane.opacity
)
return
}
drawScaledText(
context,
line,
line ?: return,
rowLeft,
rowTop - 2,
opacity = pane.opacity
)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
return super.mouseClicked(mouseX, mouseY, button)
}
}
}
}
@@ -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
}
}
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.client.net.battle
import com.cobblemon.mod.common.client.battle.ClientBattleInformationRepository
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.battle.ActiveClientBattlePokemon
@@ -20,6 +22,9 @@ import net.minecraft.client.Minecraft
object BattleInitializeHandler : ClientNetworkPacketHandler<BattleInitializePacket> {
override fun handle(packet: BattleInitializePacket, client: Minecraft) {
ClientBattleInformationRepository.clear()
packet.side1.actors.forEach { ClientBattleInformationRepository.actorActualSides[it.uuid] = 1 }
packet.side2.actors.forEach { ClientBattleInformationRepository.actorActualSides[it.uuid] = 2 }
val playerUUID = Minecraft.getInstance().player?.uuid
CobblemonClient.battle = ClientBattle(
packet.battleId,
@@ -21,6 +21,7 @@ import net.minecraft.locale.Language
object BattleMessageHandler : ClientNetworkPacketHandler<BattleMessagePacket> {
override fun handle(packet: BattleMessagePacket, client: Minecraft) {
val battle = CobblemonClient.battle ?: return
battle.messages.addComponents(packet.messages)
val textRenderer = Minecraft.getInstance().font
for (message in packet.messages) {
val line = message.copy().bold().font(CobblemonResources.DEFAULT_LARGE)
@@ -28,4 +29,4 @@ object BattleMessageHandler : ClientNetworkPacketHandler<BattleMessagePacket> {
battle.messages.add(lines)
}
}
}
}
@@ -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
)))
}
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.events
import com.cobblemon.mod.common.battles.TeamPreviewManager
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.util.party
import net.minecraft.server.MinecraftServer
@@ -16,6 +18,7 @@ object ServerTickHandler {
private var secondsTick = 0
fun onTick(server: MinecraftServer) {
TeamPreviewManager.tick()
BattleRegistry.tick()
secondsTick++
@@ -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, forceReveal = true) } }
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,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()
)
}
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.net.serverhandling.battle
import com.cobblemon.mod.common.net.messages.client.battle.BattleActorTeamPacket
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
@@ -79,6 +81,10 @@ object SpectateBattleHandler : ServerNetworkPacketHandler<SpectateBattlePacket>
battle.spectators.add(player.uuid)
player.sendPacket(BattleInitializePacket(battle, null))
player.sendPacket(BattleMessagePacket(battle.chatLog))
battle.notifyOfUpdates(listOf(player.uuid))
battle.actors.forEach { actor ->
player.sendPacket(BattleActorTeamPacket(actor.uuid, actor.pokemonList.map { it.toBattleDTO(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.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 net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object TeamPreviewSelectPokemonHandler : ServerNetworkPacketHandler<TeamPreviewSelectPokemonPacket> {
override fun handle(packet: TeamPreviewSelectPokemonPacket, server: MinecraftServer, player: ServerPlayer) {
TeamPreviewManager.changeSelections(player.uuid, packet.selected)
}
}
@@ -0,0 +1,75 @@
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "cobblemon:battle_hail",
"basic_render_parameters": {
"material": "particles_blend",
"texture": "textures/particles/weather/battle_hail"
}
},
"curves": {
"variable.size": {
"type": "linear",
"input": "v.particle_age",
"horizontal_range": "v.particle_lifetime",
"nodes": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
}
},
"events": {
"event_pieces": {
"particle_effect": {
"effect": "cobblemon:battle_hail_pieces",
"type": "particle_with_velocity"
}
}
},
"components": {
"minecraft:emitter_rate_steady": {
"spawn_rate": 75,
"max_particles": 600
},
"minecraft:emitter_lifetime_expression": {
"activation_expression": 1,
"expiration_expression": "q.is_weather_active == 0"
},
"minecraft:emitter_shape_disc": {
"offset": [0, 32, 0],
"radius": 16,
"direction": [0, -1, 0]
},
"minecraft:particle_lifetime_expression": {
"max_lifetime": 2
},
"minecraft:particle_initial_speed": 48,
"minecraft:particle_motion_dynamic": {
"linear_acceleration": [0, -24, 0]
},
"minecraft:particle_appearance_billboard": {
"size": ["0.4 * v.size", "0.4 * v.size"],
"facing_camera_mode": "rotate_xyz",
"uv": {
"texture_width": 32,
"texture_height": 8,
"uv": ["math.floor(v.particle_random_3*4)*8", 0],
"uv_size": [8, 8]
}
},
"minecraft:particle_motion_collision": {
"collision_drag": 1000,
"coefficient_of_restitution": 0.07,
"collision_radius": 0.15
},
"minecraft:particle_appearance_tinting": {
"color": {
"interpolant": "v.particle_age / v.particle_lifetime",
"gradient": {
"0.0": "#00FFFFFF",
"0.16": "#ffffffff",
"1.0": "#FFFFFFFF"
}
}
}
}
}
}
@@ -0,0 +1,49 @@
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "cobblemon:battle_hail_pieces",
"basic_render_parameters": {
"material": "particles_alpha",
"texture": "textures/particles/weather/battle_hail_pieces"
}
},
"components": {
"minecraft:emitter_rate_instant": {
"num_particles": 2
},
"minecraft:emitter_lifetime_once": {
"active_time": 1
},
"minecraft:emitter_shape_point": {
"direction": ["math.random(-0.6,0.6)", 0.2, "math.random(-0.6,0.6)"]
},
"minecraft:particle_lifetime_expression": {
"max_lifetime": 0.5
},
"minecraft:particle_initial_spin": {
"rotation": "math.random_integer(1,4) * 90"
},
"minecraft:particle_initial_speed": 5,
"minecraft:particle_motion_dynamic": {
"linear_acceleration": [0, -10, 0],
"linear_drag_coefficient": 2
},
"minecraft:particle_appearance_billboard": {
"size": [0.4, 0.4],
"facing_camera_mode": "rotate_xyz",
"uv": {
"texture_width": 32,
"texture_height": 8,
"uv": ["math.floor(variable.particle_random_2 * 4) * 8", 0],
"uv_size": [8, 8]
}
},
"minecraft:particle_motion_collision": {
"collision_drag": 0.6,
"coefficient_of_restitution": 1,
"collision_radius": 0.01
}
}
}
}
@@ -0,0 +1,69 @@
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "cobblemon:battle_rain",
"basic_render_parameters": {
"material": "particles_blend",
"texture": "textures/particles/weather/battle_rain"
}
},
"events": {
"event_splash": {
"particle_effect": {
"effect": "cobblemon:battle_splash",
"type": "particle"
}
}
},
"components": {
"minecraft:emitter_rate_steady": {
"spawn_rate": 300,
"max_particles": 600
},
"minecraft:emitter_lifetime_expression": {
"activation_expression": 1,
"expiration_expression": "q.is_weather_active == 0"
},
"minecraft:emitter_shape_disc": {
"offset": [0, 32, 0],
"radius": 16,
"direction": ["v.particle_random_1 * -v.particle_random_1", -1, "v.particle_random_1 * -v.particle_random_1"]
},
"minecraft:particle_lifetime_expression": {
"max_lifetime": 2
},
"minecraft:particle_lifetime_events": {
"expiration_event": ["event_splash", "event_smoke"]
},
"minecraft:particle_initial_speed": 8,
"minecraft:particle_motion_dynamic": {
"linear_acceleration": [0, -32, 0]
},
"minecraft:particle_appearance_billboard": {
"size": [0.2, 0.5],
"facing_camera_mode": "lookat_direction",
"uv": {
"texture_width": 16,
"texture_height": 16,
"uv": [0, 0],
"uv_size": [16, 16]
}
},
"minecraft:particle_motion_collision": {
"collision_radius": 0.1,
"expire_on_contact": true
},
"minecraft:particle_appearance_tinting": {
"color": {
"interpolant": "v.particle_age / v.particle_lifetime",
"gradient": {
"0.0": "#00FFFFFF",
"0.48": "#ffffffff",
"1.0": "#FFFFFFFF"
}
}
}
}
}
}
@@ -0,0 +1,88 @@
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "cobblemon:battle_sandstorm",
"basic_render_parameters": {
"material": "particles_blend",
"texture": "textures/particles/weather/battle_sand"
}
},
"curves": {
"variable.sway": {
"type": "linear",
"input": "v.particle_age",
"horizontal_range": "v.particle_lifetime",
"nodes": [-1, 1, -1, 1, -1]
}
},
"events": {
"event_dust": {
"particle_effect": {
"effect": "cobblemon:battle_sandstorm_dust",
"type": "particle"
}
},
"event_obscure": {
"particle_effect": {
"effect": "cobblemon:battle_sandstorm_obscure",
"type": "particle"
}
}
},
"components": {
"minecraft:emitter_rate_steady": {
"spawn_rate": 200,
"max_particles": 600
},
"minecraft:emitter_lifetime_expression": {
"activation_expression": 1,
"expiration_expression": "q.is_weather_active == 0"
},
"minecraft:emitter_lifetime_events": {
"creation_event": ["event_dust", "event_obscure"]
},
"minecraft:emitter_shape_sphere": {
"offset": [-10, "math.random(0,8)", 0],
"radius": 8,
"direction": [6, -0.2, "math.random(-1,1)"]
},
"minecraft:particle_lifetime_expression": {
"max_lifetime": 1
},
"minecraft:particle_initial_spin": {
"rotation": "math.random_integer(1,4)*90"
},
"minecraft:particle_initial_speed": "math.random(16,32)",
"minecraft:particle_motion_dynamic": {
"linear_acceleration": ["variable.sway + math.random(-1,1)", "-v.particle_random_1 * math.random(10,21)", 0],
"linear_drag_coefficient": 0.05
},
"minecraft:particle_appearance_billboard": {
"size": [0.6, 0.6],
"facing_camera_mode": "rotate_xyz",
"uv": {
"texture_width": 8,
"texture_height": 32,
"uv": [0, "math.floor(variable.particle_random_2 * 4) * 8"],
"uv_size": [8, 8]
}
},
"minecraft:particle_motion_collision": {
"collision_radius": 0.05,
"expire_on_contact": true
},
"minecraft:particle_appearance_tinting": {
"color": {
"interpolant": "v.particle_age / v.particle_lifetime",
"gradient": {
"0.0": "#00FFFFFF",
"0.18": "#FFFFFFFF",
"0.82": "#FFFFFFFF",
"1.0": "#00FFFFFF"
}
}
}
}
}
}
@@ -0,0 +1,59 @@
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "cobblemon:battle_sandstorm_dust",
"basic_render_parameters": {
"material": "particles_blend",
"texture": "textures/particles/weather/battle_sand_dust"
}
},
"components": {
"minecraft:emitter_rate_steady": {
"spawn_rate": 200,
"max_particles": 300
},
"minecraft:emitter_lifetime_expression": {
"activation_expression": 1,
"expiration_expression": "q.is_weather_active == 0"
},
"minecraft:emitter_shape_disc": {
"offset": [0, "math.random(0,8)", 0],
"radius": "math.random(8,16)",
"direction": [8, 1, "math.random(-2,2)"]
},
"minecraft:particle_lifetime_expression": {
"max_lifetime": "math.random(0.7,1.6)"
},
"minecraft:particle_initial_spin": {
"rotation": "math.random_integer(1,4) * 90"
},
"minecraft:particle_initial_speed": 8,
"minecraft:particle_motion_dynamic": {
"linear_acceleration": [0, 1, 0],
"linear_drag_coefficient": 1
},
"minecraft:particle_appearance_billboard": {
"size": [0.7, 0.7],
"facing_camera_mode": "rotate_xyz",
"uv": {
"texture_width": 64,
"texture_height": 16,
"uv": ["math.floor(v.particle_random_2 * 4) * 16", 0],
"uv_size": [16, 16]
}
},
"minecraft:particle_appearance_tinting": {
"color": {
"interpolant": "v.particle_age / v.particle_lifetime",
"gradient": {
"0.0": "#00FFFFFF",
"0.24": "#8FFFFFFF",
"0.69": "#6EFFFFFF",
"1.0": "#00FFFFFF"
}
}
}
}
}
}
@@ -0,0 +1,59 @@
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "cobblemon:battle_sandstorm_obscure",
"basic_render_parameters": {
"material": "particles_blend",
"texture": "textures/particles/weather/battle_sand_dust"
}
},
"components": {
"minecraft:emitter_rate_steady": {
"spawn_rate": 200,
"max_particles": 300
},
"minecraft:emitter_lifetime_expression": {
"activation_expression": 1,
"expiration_expression": "q.is_weather_active == 0"
},
"minecraft:emitter_shape_disc": {
"offset": [-8, "math.random(0,8)", 0],
"radius": "math.random(4,8)",
"direction": [6, 1, "math.random(-2,2)"]
},
"minecraft:particle_lifetime_expression": {
"max_lifetime": "math.random(0.7,1.6)"
},
"minecraft:particle_initial_spin": {
"rotation": "math.random_integer(1,4) * 90"
},
"minecraft:particle_initial_speed": 12,
"minecraft:particle_motion_dynamic": {
"linear_acceleration": [0, 1, 0],
"linear_drag_coefficient": 1
},
"minecraft:particle_appearance_billboard": {
"size": [0.7, 0.7],
"facing_camera_mode": "rotate_xyz",
"uv": {
"texture_width": 64,
"texture_height": 16,
"uv": ["math.floor(v.particle_random_2 * 4) * 16", 0],
"uv_size": [16, 16]
}
},
"minecraft:particle_appearance_tinting": {
"color": {
"interpolant": "v.particle_age / v.particle_lifetime",
"gradient": {
"0.0": "#00FFFFFF",
"0.24": "#8FFFFFFF",
"0.69": "#6EFFFFFF",
"1.0": "#00FFFFFF"
}
}
}
}
}
}
@@ -0,0 +1,71 @@
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "cobblemon:battle_snow",
"basic_render_parameters": {
"material": "particles_blend",
"texture": "textures/particles/weather/battle_snow"
}
},
"curves": {
"variable.sway": {
"type": "linear",
"input": "v.particle_age",
"horizontal_range": "v.particle_lifetime",
"nodes": [-1, 1, -1, 1, -1]
}
},
"components": {
"minecraft:emitter_rate_steady": {
"spawn_rate": 100,
"max_particles": 600
},
"minecraft:emitter_lifetime_expression": {
"activation_expression": 1,
"expiration_expression": "q.is_weather_active == 0"
},
"minecraft:emitter_lifetime_events": {
"creation_event": "event_snow_dust"
},
"minecraft:emitter_shape_disc": {
"offset": [0, "math.random(1,32)", 0],
"radius": 16,
"direction": ["math.random(-1,1)", -2, "math.random(-1,1)"]
},
"minecraft:particle_lifetime_expression": {
"max_lifetime": 6
},
"minecraft:particle_initial_speed": "math.random(1,2)",
"minecraft:particle_motion_dynamic": {
"linear_acceleration": ["variable.sway + math.random(-1,1)", "v.particle_random_1 * math.random(-4,4)", 0],
"linear_drag_coefficient": 0.05
},
"minecraft:particle_appearance_billboard": {
"size": [0.1, 0.1],
"facing_camera_mode": "rotate_y",
"uv": {
"texture_width": 16,
"texture_height": 4,
"uv": ["math.floor(variable.particle_random_2 * 4) * 4", 0],
"uv_size": [4, 4]
}
},
"minecraft:particle_motion_collision": {
"collision_radius": 0.05,
"expire_on_contact": true
},
"minecraft:particle_appearance_tinting": {
"color": {
"interpolant": "(v.particle_age / v.particle_lifetime)",
"gradient": {
"0.0": "#00FFFFFF",
"0.26": "#ffffffff",
"0.77": "#ffffffff",
"1.0": "#00FFFFFF"
}
}
}
}
}
}
@@ -0,0 +1,41 @@
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "cobblemon:battle_splash",
"basic_render_parameters": {
"material": "particles_alpha",
"texture": "textures/particles/weather/battle_rain_splash"
}
},
"components": {
"minecraft:emitter_rate_instant": {
"num_particles": 1
},
"minecraft:emitter_lifetime_once": {
"active_time": 1
},
"minecraft:emitter_shape_point": {
"direction": ["math.random(-0.6,0.6)", 1, "math.random(-0.6,0.6)"]
},
"minecraft:particle_lifetime_expression": {
"max_lifetime": 1
},
"minecraft:particle_initial_speed": 5,
"minecraft:particle_motion_dynamic": {
"linear_acceleration": [0, -10, 0],
"linear_drag_coefficient": 2
},
"minecraft:particle_appearance_billboard": {
"size": [0.2, 0.2],
"facing_camera_mode": "rotate_xyz",
"uv": {
"texture_width": 8,
"texture_height": 32,
"uv": [0, "math.floor(variable.particle_random_2 * 4) * 8"],
"uv_size": [8, 8]
}
}
}
}
}
@@ -9477,5 +9477,122 @@
"stat.cobblemon.reel_ins": "PokéRod Catches",
"stat.cobblemon.riding_land": "Distance by Pokémon on Land",
"stat.cobblemon.riding_air": "Distance by Pokémon in Air",
"stat.cobblemon.riding_liquid": "Distance by Pokémon in Liquid"
"stat.cobblemon.riding_liquid": "Distance by Pokémon in Liquid",
"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",
"deltaclient.ui.battle.label.buffs": "Buffs/Debuffs",
"deltaclient.ui.battle.label.current_hp": "Current HP",
"deltaclient.ui.battle.label.ability": "Ability",
"deltaclient.ui.battle.label.moves": "Moves",
"deltaclient.ui.battle.label.held_item": "Held Item",
"deltaclient.ui.battle.label.speed_tier": "Speed Tier",
"deltaclient.ui.battle.label.pokemon_form": "Pokémon Form",
"deltaclient.ui.battle.effectiveness.has_no_effect": "Has no effect",
"deltaclient.ui.battle.effectiveness.mostly_ineffective": "Mostly ineffective",
"deltaclient.ui.battle.effectiveness.not_very_effective": "Not very effective",
"deltaclient.ui.battle.effectiveness.effective": "Effective",
"deltaclient.ui.battle.effectiveness.super_effective": "Super effective",
"deltaclient.ui.battle.effectiveness.extremely_effective": "Extremely effective",
"deltaclient.ui.battle.turn": "Turn %1$s",
"deltaclient.ui.battle.stats.special_attack": "Sp. Attack",
"deltaclient.ui.battle.stats.special_defence": "Sp. Defense",
"deltaclient.ui.battle.speed_tier": "%1$s to %2$s",
"deltaclient.ui.battle.effect.raindance": "Rain — %1$s turn(s) left",
"deltaclient.ui.battle.effect.raindance.range": "Rain — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.effect.primordialsea": "Heavy Rain",
"deltaclient.ui.battle.effect.sandstorm": "Sandstorm — %1$s turn(s) left",
"deltaclient.ui.battle.effect.sandstorm.range": "Sandstorm — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.effect.sunnyday": "Sun — %1$s turn(s) left",
"deltaclient.ui.battle.effect.sunnyday.range": "Sun — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.effect.desolateland": "Intense Sun",
"deltaclient.ui.battle.effect.hail": "Hail — %1$s turn(s) left",
"deltaclient.ui.battle.effect.hail.range": "Hail — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.effect.snow": "Snow — %1$s turn(s) left",
"deltaclient.ui.battle.effect.snow.range": "Snow — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.effect.deltastream": "Delta Stream",
"deltaclient.ui.battle.effect.trickroom": "Trick Room — %1$s turn(s) left",
"deltaclient.ui.battle.effect.magicroom": "Magic Room — %1$s turn(s) left",
"deltaclient.ui.battle.effect.wonderroom": "Wonder Room — %1$s turn(s) left",
"deltaclient.ui.battle.effect.electricterrain": "Electric Terrain — %1$s turn(s) left",
"deltaclient.ui.battle.effect.electricterrain.range": "Electric Terrain — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.effect.mistyterrain": "Misty Terrain — %1$s turn(s) left",
"deltaclient.ui.battle.effect.mistyterrain.range": "Misty Terrain — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.effect.psychicterrain": "Psychic Terrain — %1$s turn(s) left",
"deltaclient.ui.battle.effect.psychicterrain.range": "Psychic Terrain — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.effect.grassyterrain": "Grassy Terrain — %1$s turn(s) left",
"deltaclient.ui.battle.effect.grassyterrain.range": "Grassy Terrain — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.effect.corrosiveterrain": "Corrosive Terrain — %1$s turn(s) left",
"deltaclient.ui.battle.effect.corrosiveterrain.range": "Corrosive Terrain — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.sided_effect.sharpsteel": "Sharp Steel",
"deltaclient.ui.battle.sided_effect.spikes": "Spikes",
"deltaclient.ui.battle.sided_effect.stealthrock": "Stealth Rock",
"deltaclient.ui.battle.sided_effect.stickyweb": "Sticky Web",
"deltaclient.ui.battle.sided_effect.toxicspikes": "Toxic Spikes",
"deltaclient.ui.battle.sided_effect.reflect": "Reflect — %1$s turn(s) left",
"deltaclient.ui.battle.sided_effect.reflect.range": "Reflect — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.sided_effect.lightscreen": "Light Screen — %1$s turn(s) left",
"deltaclient.ui.battle.sided_effect.lightscreen.range": "Light Screen — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.sided_effect.auroraveil": "Aurora Veil — %1$s turn(s) left",
"deltaclient.ui.battle.sided_effect.auroraveil.range": "Aurora Veil — %1$s to %2$s turn(s) left",
"deltaclient.ui.battle.sided_effect.tailwind": "Tailwind — %1$s turn(s) left",
"deltaclient.ui.battle.team_info": "Team Information",
"deltaclient.ui.battle.team_info.button": "Info"
}
@@ -9212,5 +9212,62 @@
"stat.cobblemon.riding_air": "Distance parcourue par Pokémon dans les airs",
"stat.cobblemon.riding_liquid": "Distance parcourue par Pokémon dans un liquide",
"effect.cobblemon.cleanse_all": "Nettoyage des effets",
"effect.cobblemon.cleanse_negative": "Nettoyage des effets négatifs"
"effect.cobblemon.cleanse_negative": "Nettoyage des effets négatifs",
"cobblemon.battle.ui.effect.desolateland": "Terre Finale",
"cobblemon.battle.ui.effect.electricterrain": "Champ Électrifié",
"cobblemon.battle.ui.effect.electricterrain.range": "Champ Électrifié ",
"cobblemon.battle.ui.effect.grassyterrain": "Champ Herbu",
"cobblemon.battle.ui.effect.grassyterrain.range": "Champ Herbu ",
"cobblemon.battle.ui.effect.hail": "Grêle",
"cobblemon.battle.ui.effect.hail.range": "Grêle ",
"cobblemon.battle.ui.effect.magicroom": "Zone Magique",
"cobblemon.battle.ui.effect.mistyterrain": "Champ Brumeux",
"cobblemon.battle.ui.effect.mistyterrain.range": "Champ Brumeux ",
"cobblemon.battle.ui.effect.primordialsea": "Mer Primaire",
"cobblemon.battle.ui.effect.psychicterrain": "Champ Psychique",
"cobblemon.battle.ui.effect.psychicterrain.range": "Champ Psychique ",
"cobblemon.battle.ui.effect.raindance": "Danse Pluie",
"cobblemon.battle.ui.effect.raindance.range": "Danse Pluie ",
"cobblemon.battle.ui.effect.sandstorm": "Tempête de Sable",
"cobblemon.battle.ui.effect.sandstorm.range": "Tempête de Sable ",
"cobblemon.battle.ui.effect.snow": "Neige",
"cobblemon.battle.ui.effect.snow.range": "Neige ",
"cobblemon.battle.ui.effect.sunnyday": "Zénith",
"cobblemon.battle.ui.effect.sunnyday.range": "Zénith ",
"cobblemon.battle.ui.effect.trickroom": "Distorsion",
"cobblemon.battle.ui.effect.wonderroom": "Zone Étrange",
"cobblemon.battle.ui.effectiveness.effective": "Efficace",
"cobblemon.battle.ui.effectiveness.extremely_effective": "Extrêmement Efficace !",
"cobblemon.battle.ui.effectiveness.has_no_effect": "Aucun Effet",
"cobblemon.battle.ui.effectiveness.mostly_ineffective": "Très Peu Efficace",
"cobblemon.battle.ui.effectiveness.not_very_effective": "Peu Efficace",
"cobblemon.battle.ui.effectiveness.super_effective": "Super Efficace !",
"cobblemon.battle.ui.label.ability": "Talent",
"cobblemon.battle.ui.label.buffs": "Boosts",
"cobblemon.battle.ui.label.held_item": "Objet Tenu",
"cobblemon.battle.ui.label.moves": "Capacités",
"cobblemon.battle.ui.label.pokemon_form": "Forme du Pokémon",
"cobblemon.battle.ui.label.speed_tier": "Niveau de Vitesse",
"cobblemon.battle.ui.sided_effect.auroraveil": "Voile Aurore",
"cobblemon.battle.ui.sided_effect.auroraveil.range": "Voile Aurore (%1$s tours)",
"cobblemon.battle.ui.sided_effect.lightscreen": "Mur Lumière",
"cobblemon.battle.ui.sided_effect.lightscreen.range": "Mur Lumière (%1$s tours)",
"cobblemon.battle.ui.sided_effect.reflect": "Reflet",
"cobblemon.battle.ui.sided_effect.reflect.range": "Reflet (%1$s tours)",
"cobblemon.battle.ui.sided_effect.sharpsteel": "Piège d\u0027Acier ",
"cobblemon.battle.ui.sided_effect.spikes": "Picots",
"cobblemon.battle.ui.sided_effect.stealthrock": "Piège de Roc",
"cobblemon.battle.ui.sided_effect.stickyweb": "Toile Gluante",
"cobblemon.battle.ui.sided_effect.tailwind": "Vent Arrière",
"cobblemon.battle.ui.sided_effect.toxicspikes": "Pics Toxik",
"cobblemon.battle.ui.speed_tier": "Niveau de Vitesse : %1$s",
"cobblemon.battle.ui.stats.special_attack": "Attaque Spéciale",
"cobblemon.battle.ui.stats.special_defence": "Défense Spéciale",
"cobblemon.battle.ui.team_info": "Informations sur l\u0027Équipe",
"cobblemon.battle.ui.team_info.button": "Équipe",
"cobblemon.battle.ui.team_info.title": "Informations sur l\u0027Équipe",
"cobblemon.battle.ui.team_preview.title": "Aperçu de l\u0027Équipe",
"cobblemon.battle.ui.team_preview.waiting": "En attente de l\u0027adversaire...",
"cobblemon.battle.ui.turn": "Tour %1$s",
"cobblemon.battle.ui.turn.timer": "Temps restant : %1$s"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

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: 223 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

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