Added health sliding animations, implemented battle dispatching for delays between actions, fixed 'no battle found' error

This commit is contained in:
Paul
2022-06-05 11:02:05 +10:00
parent e9ab56aebd
commit 9ca841594e
16 changed files with 435 additions and 74 deletions
@@ -85,6 +85,7 @@ object CobbledNetwork {
buildClientMessage<BattleQueueRequestPacket>()
buildClientMessage<BattleFaintPacket>()
buildClientMessage<BattleMakeChoicePacket>()
buildClientMessage<BattleHealthChangePacket>()
/**
* Server Packets
@@ -179,6 +179,7 @@ class PokemonBattle(
val readyToInput = actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose }
if (readyToInput) {
actors.filter { it.responses.isNotEmpty() }.forEach { it.writeShowdownResponse() }
actors.forEach { it.responses.clear() ; it.request = null }
}
}
}
@@ -81,28 +81,22 @@ abstract class ShowdownActionResponse(val type: ShowdownActionResponseType) {
data class MoveActionResponse(var moveName: String, var targetPnx: String? = null): ShowdownActionResponse(ShowdownActionResponseType.MOVE) {
override fun isValid(activeBattlePokemon: ActiveBattlePokemon, showdownMoveSet: ShowdownMoveset?, forceSwitch: Boolean): Boolean {
if (forceSwitch || showdownMoveSet == null) {
println("A")
return false
}
val move = showdownMoveSet.moves.find { it.id == moveName } ?: return false
if (!move.canBeUsed()) {
println("B")
// No PP or disabled or something
return false
}
val availableTargets = move.target.targetList(activeBattlePokemon) ?: return true
println("C")
val pnx = targetPnx ?: return false // If the targets list is non-null then they need to have specified a target
println("D")
val (_, targetPokemon) = activeBattlePokemon.actor.battle.getActorAndActiveSlotFromPNX(pnx)
if (targetPokemon !in availableTargets || targetPokemon.battlePokemon == null || targetPokemon.battlePokemon!!.health <= 0) {
println("E")
return false // It's not a possible target.
}
println("F")
return true
}
@@ -6,12 +6,11 @@ import com.cablemc.pokemoncobbled.common.api.battles.model.PokemonBattle
import com.cablemc.pokemoncobbled.common.api.battles.model.actor.BattleActor
import com.cablemc.pokemoncobbled.common.api.text.*
import com.cablemc.pokemoncobbled.common.battles.actor.PlayerBattleActor
import com.cablemc.pokemoncobbled.common.battles.dispatch.DispatchResult
import com.cablemc.pokemoncobbled.common.battles.dispatch.GO
import com.cablemc.pokemoncobbled.common.battles.dispatch.WaitDispatch
import com.cablemc.pokemoncobbled.common.battles.runner.ShowdownConnection
import com.cablemc.pokemoncobbled.common.net.messages.client.battle.BattleFaintPacket
import com.cablemc.pokemoncobbled.common.net.messages.client.battle.BattleInitializePacket
import com.cablemc.pokemoncobbled.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cablemc.pokemoncobbled.common.net.messages.client.battle.BattleQueueRequestPacket
import com.cablemc.pokemoncobbled.common.net.messages.client.battle.*
import com.cablemc.pokemoncobbled.common.util.battleLang
import com.cablemc.pokemoncobbled.common.util.getPlayer
import net.minecraft.text.LiteralText
@@ -52,6 +51,12 @@ object ShowdownInterpreter {
val battleId = message.split(ShowdownConnection.LINE_START)[0]
val rawMessage = message.replace(battleId + ShowdownConnection.LINE_START, "")
if (rawMessage.startsWith("{\"winner\":\"")) {
// The post-win message is something we don't care about just yet. It's basically a summary of what happened in the battle.
// Check /docs/example-post-win-message.json for its format.
return
}
val battle = BattleRegistry.getBattle(UUID.fromString(battleId))
if (battle == null) {
@@ -312,17 +317,20 @@ object ShowdownInterpreter {
battle.dispatch {
battle.sendToActors(BattleMakeChoicePacket())
battle.broadcastChatMessage("".text())
battle.broadcastChatMessage(">>".aqua() + " It is now turn ${message.split("|turn|")[1]}".aqua())
battle.broadcastChatMessage("".text())
battle.turn()
GO
}
battle.broadcastChatMessage("".text())
battle.broadcastChatMessage(">>".aqua() + " It is now turn ${message.split("|turn|")[1]}".aqua())
battle.broadcastChatMessage("".text())
battle.turn()
}
private fun handleUpkeepInstruction(battle: PokemonBattle, message: String) {
battle.actors.forEach { it.upkeep() }
battle.dispatch {
battle.actors.forEach { it.upkeep() }
GO
}
}
/**
@@ -338,58 +346,67 @@ object ShowdownInterpreter {
battle.sendUpdate(BattleFaintPacket(pnx, battleLang("fainted", pokemon.battlePokemon?.getName() ?: "ALREADY DEAD")))
pokemon.battlePokemon?.effectedPokemon?.currentHealth = 0
pokemon.battlePokemon = null
battle.broadcastChatMessage("".text())
battle.broadcastChatMessage(">> ".red() + battleLang("fainted", pokemon.battlePokemon?.getName() ?: "ALREADY DEAD".red()).gold())
battle.broadcastChatMessage("".text())
GO
}
battle.broadcastChatMessage("".text())
battle.broadcastChatMessage(">> ".red() + battleLang("fainted", pokemon.battlePokemon?.getName() ?: "ALREADY DEAD".red()).gold())
battle.broadcastChatMessage("".text())
}
private fun handleWinInstruction(battle: PokemonBattle, message: String) {
val ids = message.split("|win|")[1].split("&").map { it.trim() }
val winners = ids.map { battle.getActor(UUID.fromString(it))!!.getName() }.reduce { acc, next -> acc + " & " + next }
battle.dispatch {
val ids = message.split("|win|")[1].split("&").map { it.trim() }
val winners = ids.map { battle.getActor(UUID.fromString(it))!!.getName() }.reduce { acc, next -> acc + " & " + next }
battle.broadcastChatMessage(">> ".gold() + battleLang("win", winners).gold())
battle.broadcastChatMessage(">> ".gold() + battleLang("win", winners).gold())
battle.end()
BattleRegistry.closeBattle(battle)
battle.end()
BattleRegistry.closeBattle(battle)
GO
}
}
// |move|p1a: Charizard|Tackle|p2a: Magikarp
private fun handleMoveInstruction(battle: PokemonBattle, message: String) {
val editMessaged = message.replace("|move|", "")
battle.dispatch {
val editMessaged = message.replace("|move|", "")
val userPNX = editMessaged.split("|")[0].split(":")[0].trim()
val (_, userPokemon) = battle.getActorAndActiveSlotFromPNX(userPNX)
val move = editMessaged.split("|")[1].split("|")[0]
val hasTarget = editMessaged.split("|").size == 3 && editMessaged.split("|")[2].isNotEmpty()
if (hasTarget) {
val targetPNX = editMessaged.split("|")[2].split(":")[0]
val (_, targetPokemon) = battle.getActorAndActiveSlotFromPNX(targetPNX)
battle.broadcastChatMessage(">> ".yellow() + battleLang(
key = "used_move_on",
userPokemon.battlePokemon?.getName() ?: "ERROR".red(),
move,
targetPokemon.battlePokemon?.getName() ?: "ERROR".red()
))
} else {
battle.broadcastChatMessage(">> ".yellow() + battleLang(
key = "used_move_on",
userPokemon.battlePokemon?.getName() ?: "ERROR".red(),
move
))
val userPNX = editMessaged.split("|")[0].split(":")[0].trim()
val (_, userPokemon) = battle.getActorAndActiveSlotFromPNX(userPNX)
val move = editMessaged.split("|")[1].split("|")[0]
val hasTarget = editMessaged.split("|").size == 3 && editMessaged.split("|")[2].isNotEmpty()
if (hasTarget) {
val targetPNX = editMessaged.split("|")[2].split(":")[0]
val (_, targetPokemon) = battle.getActorAndActiveSlotFromPNX(targetPNX)
battle.broadcastChatMessage(">> ".yellow() + battleLang(
key = "used_move_on",
userPokemon.battlePokemon?.getName() ?: "ERROR".red(),
move,
targetPokemon.battlePokemon?.getName() ?: "ERROR".red()
))
} else {
battle.broadcastChatMessage(">> ".yellow() + battleLang(
key = "used_move_on",
userPokemon.battlePokemon?.getName() ?: "ERROR".red(),
move
))
}
GO
}
}
private fun handleCantInstruction(battle: PokemonBattle, message: String) {
val editMessaged = message.replace("|cant|", "")
battle.dispatch {
val editMessaged = message.replace("|cant|", "")
val pnx = editMessaged.split("|")[0].split(":")[0]
val (actor, pokemon) = battle.getActorAndActiveSlotFromPNX(pnx)
val action = editMessaged.split("|")[1]
val actionText = if (action == "flinch") "flinched" else action
val pnx = editMessaged.split("|")[0].split(":")[0]
val (actor, pokemon) = battle.getActorAndActiveSlotFromPNX(pnx)
val action = editMessaged.split("|")[1]
val actionText = if (action == "flinch") "flinched" else action
battle.broadcastChatMessage(">> ".red() + (pokemon.battlePokemon?.getName() ?: "DEAD".text()) + " has $actionText".red())
battle.broadcastChatMessage(">> ".red() + (pokemon.battlePokemon?.getName() ?: "DEAD".text()) + " has $actionText".red())
GO
}
}
@@ -408,9 +425,13 @@ object ShowdownInterpreter {
// Parse Json message and update state info for actor
val request = BattleRegistry.gson.fromJson(message.split("|request|")[1], ShowdownActionRequest::class.java)
if (battle.started) {
battleActor.sendUpdate(BattleQueueRequestPacket(request))
battle.dispatch {
battleActor.sendUpdate(BattleQueueRequestPacket(request))
GO
}
}
battleActor.request = request
battleActor.responses.clear()
}
private fun handleSwitchInstruction(battle: PokemonBattle, battleActor: BattleActor, publicMessage: String, privateMessage: String) {
@@ -418,17 +439,35 @@ object ShowdownInterpreter {
val (actor, activePokemon) = battle.getActorAndActiveSlotFromPNX(pnx)
val uuid = UUID.fromString(publicMessage.split("|")[2].split(":")[1].trim())
val pokemon = actor.pokemonList.find { it.uuid == uuid } ?: throw IllegalStateException("Unable to find ${actor.showdownId}'s Pokemon with UUID: $uuid")
activePokemon.battlePokemon = pokemon
if (battle.started) {
battle.dispatch {
activePokemon.battlePokemon = pokemon
GO
// TODO send packet for switching in
}
} else {
activePokemon.battlePokemon = pokemon
}
}
fun handleDamageInstruction(battle: PokemonBattle, actor: BattleActor, publicMessage: String, privateMessage: String) {
val (_, activePokemon) = battle.getActorAndActiveSlotFromPNX(publicMessage.split("|")[2].split(":")[0])
val pnx = publicMessage.split("|")[2].split(":")[0]
val (_, activePokemon) = battle.getActorAndActiveSlotFromPNX(pnx)
val newHealth = privateMessage.split("|")[3].split(" ")[0]
if (newHealth == "0") {
activePokemon.battlePokemon?.effectedPokemon?.currentHealth = 0
} else {
val remainingHealth = newHealth.split("/")[0].toInt()
activePokemon.battlePokemon?.effectedPokemon?.currentHealth = remainingHealth
battle.dispatch {
val newHealthRatio: Float
if (newHealth == "0") {
activePokemon.battlePokemon?.effectedPokemon?.currentHealth = 0
newHealthRatio = 0F
} else {
val remainingHealth = newHealth.split("/")[0].toInt()
activePokemon.battlePokemon?.effectedPokemon?.currentHealth = remainingHealth
newHealthRatio = remainingHealth.toFloat() / newHealth.split("/")[1].toInt()
}
battle.sendUpdate(BattleHealthChangePacket(pnx, newHealthRatio))
WaitDispatch(1.5F)
}
}
@@ -2,7 +2,7 @@ package com.cablemc.pokemoncobbled.common.client.battle.animations
import com.cablemc.pokemoncobbled.common.client.battle.ActiveClientBattlePokemon
class HealthChangeAnimation(val newHealthRatio: Float, val duration: Float = 2F) : TileAnimation {
class HealthChangeAnimation(val newHealthRatio: Float, val duration: Float = 1.5F) : TileAnimation {
var passedSeconds = 0F
var initialHealth = -1F
var difference = 0F
@@ -14,7 +14,7 @@ class HealthChangeAnimation(val newHealthRatio: Float, val duration: Float = 2F)
difference = newHealthRatio - initialHealth
}
passedSeconds += deltaTicks
passedSeconds += deltaTicks / 20
passedSeconds = passedSeconds.coerceAtMost(duration)
pokemon.hpRatio = initialHealth + (passedSeconds / duration) * difference
return passedSeconds == duration
@@ -5,7 +5,7 @@ import com.cablemc.pokemoncobbled.common.client.battle.ActiveClientBattlePokemon
class MoveTileOffscreenAnimation(private val duration: Float = 0.5F) : TileAnimation {
var passedSeconds = 0F
override fun invoke(activeBattlePokemon: ActiveClientBattlePokemon, deltaTicks: Float): Boolean {
passedSeconds += deltaTicks
passedSeconds += deltaTicks / 20
passedSeconds = passedSeconds.coerceAtMost(duration)
val ratio = duration / passedSeconds
val totalMovement = activeBattlePokemon.invisibleX - activeBattlePokemon.xDisplacement
@@ -99,8 +99,6 @@ class BattleMoveSelection(
} else if (targets.size == 1) {
moveSelection.battleGUI.selectAction(moveSelection.request, MoveActionResponse(move.id, targets[0].getPNX()))
}
println("Clicked ${moveTemplate.name}")
}
}
@@ -52,6 +52,7 @@ object ClientPacketRegistrar : SidedPacketRegistrar() {
registerHandler(BattleFaintHandler)
registerHandler(BattleQueueRequestHandler)
registerHandler(BattleMakeChoiceHandler)
registerHandler(BattleHealthChangeHandler)
}
}
@@ -0,0 +1,20 @@
package com.cablemc.pokemoncobbled.common.client.net.battle
import com.cablemc.pokemoncobbled.common.CobbledNetwork
import com.cablemc.pokemoncobbled.common.client.PokemonCobbledClient
import com.cablemc.pokemoncobbled.common.client.battle.animations.HealthChangeAnimation
import com.cablemc.pokemoncobbled.common.net.PacketHandler
import com.cablemc.pokemoncobbled.common.net.messages.client.battle.BattleHealthChangePacket
import net.minecraft.client.MinecraftClient
object BattleHealthChangeHandler : PacketHandler<BattleHealthChangePacket> {
override fun invoke(packet: BattleHealthChangePacket, ctx: CobbledNetwork.NetworkContext) {
MinecraftClient.getInstance().execute {
val battle = PokemonCobbledClient.battle ?: return@execute
val (actor, activePokemon) = battle.getPokemonFromPNX(packet.pnx)
activePokemon.animations.add(
HealthChangeAnimation(newHealthRatio = packet.newHealthRatio)
)
}
}
}
@@ -7,6 +7,7 @@ import com.cablemc.pokemoncobbled.common.net.messages.client.battle.BattleMakeCh
object BattleMakeChoiceHandler : PacketHandler<BattleMakeChoicePacket> {
override fun invoke(packet: BattleMakeChoicePacket, ctx: CobbledNetwork.NetworkContext) {
PokemonCobbledClient.battle?.mustChoose = true
val battle = PokemonCobbledClient.battle ?: return
battle.mustChoose = true
}
}
@@ -12,7 +12,7 @@ object BattleQueueRequestHandler : PacketHandler<BattleQueueRequestPacket> {
MinecraftClient.getInstance().execute {
val battle = PokemonCobbledClient.battle ?: return@execute
val actor = battle.side1.actors.find { it.uuid == MinecraftClient.getInstance().player?.uuid } ?: return@execute
PokemonCobbledClient.battle?.pendingActionRequests?.addAll(SingleActionRequest.composeFrom(actor, packet.request))
PokemonCobbledClient.battle?.pendingActionRequests = SingleActionRequest.composeFrom(actor, packet.request)
}
}
}
@@ -37,7 +37,7 @@ class BlastoiseModel(root: ModelPart) : PokemonPoseableModel(), HeadedFrame, Bip
override fun registerPoses() {
registerPose(
poseName = "standing",
poseTypes = setOf(PoseType.NONE, PoseType.PROFILE),
poseTypes = setOf(PoseType.NONE, PoseType.PROFILE, PoseType.PORTRAIT),
transformTicks = 10,
condition = { !it.isMoving.get() && !it.isSubmergedInWater },
idleAnimations = arrayOf(
@@ -0,0 +1,24 @@
package com.cablemc.pokemoncobbled.common.net.messages.client.battle
import com.cablemc.pokemoncobbled.common.api.net.NetworkPacket
import net.minecraft.network.PacketByteBuf
class BattleHealthChangePacket() : NetworkPacket {
lateinit var pnx: String
var newHealthRatio = 0F
constructor(pnx: String, newHealthRatio: Float): this() {
this.pnx = pnx
this.newHealthRatio = newHealthRatio
}
override fun encode(buffer: PacketByteBuf) {
buffer.writeString(pnx)
buffer.writeFloat(newHealthRatio)
}
override fun decode(buffer: PacketByteBuf) {
pnx = buffer.readString()
newHealthRatio = buffer.readFloat()
}
}
@@ -11,12 +11,10 @@ class BattleMessagePacket() : NetworkPacket {
}
override fun encode(buffer: PacketByteBuf) {
// TODO("Not yet implemented")
buffer.writeText(message)
}
override fun decode(buffer: PacketByteBuf) {
// TODO("Not yet implemented")
message = buffer.readText().copy()
}
}
@@ -55,10 +55,6 @@ open class Pokemon {
var uuid = UUID.randomUUID()
var species = PokemonSpecies.EEVEE
set(value) {
if (value == field) {
return
}
val quotient = clamp(currentHealth / hp.toFloat(), 0F, 1F)
val previousFeatureKeys = species.features
field = value
+288
View File
@@ -0,0 +1,288 @@
{
"winner": "005a04f7-4cb5-3251-9991-d8e9288e0034",
"seed": [
14112,
9040,
59282,
38999
],
"turns": 6,
"p1": "005a04f7-4cb5-3251-9991-d8e9288e0034",
"p2": "c028742d-38fd-4260-b25b-e5c4c7bc06e4",
"p1team": [
{
"name": "Charizard",
"species": "charizard",
"uuid": "d5e9de52-9275-4ea5-a1d6-cdeaa3808545",
"item": "",
"ability": "Flash Fire",
"moves": [
"Tail Whip",
"Dragon Claw",
"Tackle",
"Heat Wave"
],
"nature": "Lax",
"evs": {
"hp": 0,
"atk": 0,
"def": 0,
"spa": 0,
"spd": 0,
"spe": 0
},
"gender": "M",
"ivs": {
"hp": 20,
"atk": 20,
"def": 17,
"spa": 22,
"spd": 14,
"spe": 28
},
"level": 11,
"happiness": null,
"hpType": "",
"pokeball": "",
"gigantamax": false
},
{
"name": "Gyarados",
"species": "gyarados",
"uuid": "1748cfcc-dba7-449d-ae5e-f500b1a83c2d",
"item": "",
"ability": "Flash Fire",
"moves": [
"Tail Whip",
"Growl",
"Tackle",
"Helping Hand"
],
"nature": "Impish",
"evs": {
"hp": 0,
"atk": 0,
"def": 0,
"spa": 0,
"spd": 0,
"spe": 0
},
"gender": "M",
"ivs": {
"hp": 22,
"atk": 0,
"def": 24,
"spa": 6,
"spd": 24,
"spe": 29
},
"level": 1,
"happiness": null,
"hpType": "",
"pokeball": "",
"gigantamax": false
},
{
"name": "Charizard",
"species": "charizard",
"uuid": "7606a1b1-9fed-4962-a55a-c494a701712c",
"item": "",
"ability": "Flash Fire",
"moves": [
"Tail Whip",
"Growl",
"Tackle",
"Helping Hand"
],
"nature": "Impish",
"evs": {
"hp": 0,
"atk": 0,
"def": 0,
"spa": 0,
"spd": 0,
"spe": 0
},
"gender": "M",
"ivs": {
"hp": 10,
"atk": 10,
"def": 19,
"spa": 26,
"spd": 10,
"spe": 12
},
"level": 1,
"happiness": null,
"hpType": "",
"pokeball": "",
"gigantamax": false
},
{
"name": "Venusaur",
"species": "venusaur",
"uuid": "ace77175-c500-4c0f-9020-8ec4736c1ebd",
"item": "",
"ability": "Flash Fire",
"moves": [
"Tail Whip",
"Growl",
"Tackle",
"Helping Hand"
],
"nature": "Quiet",
"evs": {
"hp": 0,
"atk": 0,
"def": 0,
"spa": 0,
"spd": 0,
"spe": 0
},
"gender": "M",
"ivs": {
"hp": 22,
"atk": 0,
"def": 22,
"spa": 9,
"spd": 15,
"spe": 27
},
"level": 5,
"happiness": null,
"hpType": "",
"pokeball": "",
"gigantamax": false
},
{
"name": "Charizard",
"species": "charizard",
"uuid": "5205b484-9598-4579-a62f-917738e1c4f7",
"item": "",
"ability": "Flash Fire",
"moves": [
"Dragon Claw",
"Growl",
"Ember",
"Smokescreen"
],
"nature": "Adamant",
"evs": {
"hp": 0,
"atk": 0,
"def": 0,
"spa": 0,
"spd": 0,
"spe": 0
},
"gender": "M",
"ivs": {
"hp": 6,
"atk": 7,
"def": 29,
"spa": 9,
"spd": 22,
"spe": 30
},
"level": 1,
"happiness": null,
"hpType": "",
"pokeball": "",
"gigantamax": false
},
{
"name": "Blastoise",
"species": "blastoise",
"uuid": "ab63689c-b4e7-4c89-b720-9fc7f8e2fe05",
"item": "",
"ability": "Flash Fire",
"moves": [
"Water Gun",
"Tail Whip",
"Tackle",
"Flash Cannon"
],
"nature": "Modest",
"evs": {
"hp": 0,
"atk": 0,
"def": 0,
"spa": 0,
"spd": 0,
"spe": 0
},
"gender": "M",
"ivs": {
"hp": 16,
"atk": 30,
"def": 13,
"spa": 15,
"spd": 26,
"spe": 26
},
"shiny": true,
"level": 1,
"happiness": null,
"hpType": "",
"pokeball": "",
"gigantamax": false
}
],
"p2team": [
{
"name": "Caterpie",
"species": "caterpie",
"uuid": "c028742d-38fd-4260-b25b-e5c4c7bc06e4",
"item": "",
"ability": "Flash Fire",
"moves": [
"String Shot",
"Tackle"
],
"nature": "Naive",
"evs": {
"hp": 0,
"atk": 0,
"def": 0,
"spa": 0,
"spd": 0,
"spe": 0
},
"gender": "M",
"ivs": {
"hp": 30,
"atk": 30,
"def": 17,
"spa": 0,
"spd": 19,
"spe": 2
},
"level": 1,
"happiness": null,
"hpType": "",
"pokeball": "",
"gigantamax": false
}
],
"score": [
6,
0
],
"inputLog": [
">start {\"seed\":[14112,9040,59282,38999]}",
">player p1 {\"name\":\"005a04f7-4cb5-3251-9991-d8e9288e0034\",\"team\":\"charizard||d5e9de52-9275-4ea5-a1d6-cdeaa3808545||flashfire|tailwhip,dragonclaw,tackle,heatwave|lax|0,0,0,0,0,0|M|20,20,17,22,14,28||11|0|||]gyarados||1748cfcc-dba7-449d-ae5e-f500b1a83c2d||flashfire|tailwhip,growl,tackle,helpinghand|impish|0,0,0,0,0,0|M|22,0,24,6,24,29||1|0|||]charizard||7606a1b1-9fed-4962-a55a-c494a701712c||flashfire|tailwhip,growl,tackle,helpinghand|impish|0,0,0,0,0,0|M|10,10,19,26,10,12||1|0|||]venusaur||ace77175-c500-4c0f-9020-8ec4736c1ebd||flashfire|tailwhip,growl,tackle,helpinghand|quiet|0,0,0,0,0,0|M|22,0,22,9,15,27||5|0|||]charizard||5205b484-9598-4579-a62f-917738e1c4f7||flashfire|dragonclaw,growl,ember,smokescreen|adamant|0,0,0,0,0,0|M|6,7,29,9,22,30||1|0|||]blastoise||ab63689c-b4e7-4c89-b720-9fc7f8e2fe05||flashfire|watergun,tailwhip,tackle,flashcannon|modest|0,0,0,0,0,0|M|16,30,13,15,26,26|S|1|0|||\"}",
">player p2 {\"name\":\"c028742d-38fd-4260-b25b-e5c4c7bc06e4\",\"team\":\"caterpie||c028742d-38fd-4260-b25b-e5c4c7bc06e4||flashfire|stringshot,tackle|naive|0,0,0,0,0,0|M|30,30,17,0,19,2||1|0|||\"}",
">p1 move heatwave",
">p2 move stringshot",
">p1 move heatwave",
">p2 move tackle",
">p1 move heatwave",
">p2 move tackle",
">p1 move heatwave",
">p2 move tackle",
">p1 move heatwave",
">p2 move tackle",
">p1 move dragonclaw",
">p2 move stringshot"
]
}