Compare commits

...
Author SHA1 Message Date
Ronak Chavva 166cc32a24 help button wip 2026-10-29 02:04:05 -05:00
Ronak Chavva 4980ffc151 feat(*): gear bg change, info icon, scrolling implementation 2026-10-28 16:39:38 -05:00
Ronak Chavva fb7d2a96b3 fix(*): cancel movement, edge movement fix 2026-10-28 14:46:53 -05:00
Ronak Chavva f6d3071a44 fix(*): pathing + range bugs 2026-10-28 14:34:07 -05:00
Ronak Chavva 73f27ec732 ui done 2026-10-28 04:18:27 -05:00
31 changed files with 1054 additions and 151 deletions

No files matched your search

@@ -42,9 +42,14 @@ import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
import com.cobblemon.mod.common.client.net.pasture.ClosePastureHandler
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
import com.cobblemon.mod.common.client.net.pasture.UpdatePastureRangeHandler
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureRangePacket
import com.cobblemon.mod.common.net.messages.server.pasture.AdjustPastureRangePacket
import com.cobblemon.mod.common.net.serverhandling.pasture.AdjustPastureRangeHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonPasturedHandler
import com.cobblemon.mod.common.client.net.pasture.PokemonUnpasturedHandler
import com.cobblemon.mod.common.client.net.pasture.UpdatePastureConflictFlagHandler
import com.cobblemon.mod.common.client.net.pasture.UpdatePastureRecoveryHandler
import com.cobblemon.mod.common.client.net.pokedex.ServerConfirmedRegisterHandler
import com.cobblemon.mod.common.client.net.pokemon.update.ClientboundSeatAssignmentPacketHandler
import com.cobblemon.mod.common.client.net.pokemon.update.ClientboundUpdateRidingStateHandler
@@ -101,6 +106,7 @@ import com.cobblemon.mod.common.net.messages.client.pasture.OpenPasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonPasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonUnpasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureConflictFlagPacket
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureRecoveryPacket
import com.cobblemon.mod.common.net.messages.client.pokedex.ServerConfirmedRegisterPacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.*
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.AddEvolutionPacket
@@ -473,10 +479,12 @@ object CobblemonNetwork {
// Pasture
list.add(PacketRegisterInfo(OpenPasturePacket.ID, OpenPasturePacket::decode, OpenPastureHandler))
list.add(PacketRegisterInfo(UpdatePastureRangePacket.ID, UpdatePastureRangePacket::decode, UpdatePastureRangeHandler))
list.add(PacketRegisterInfo(ClosePasturePacket.ID, ClosePasturePacket::decode, ClosePastureHandler))
list.add(PacketRegisterInfo(PokemonPasturedPacket.ID, PokemonPasturedPacket::decode, PokemonPasturedHandler))
list.add(PacketRegisterInfo(PokemonUnpasturedPacket.ID, PokemonUnpasturedPacket::decode, PokemonUnpasturedHandler))
list.add(PacketRegisterInfo(UpdatePastureConflictFlagPacket.ID, UpdatePastureConflictFlagPacket::decode, UpdatePastureConflictFlagHandler))
list.add(PacketRegisterInfo(UpdatePastureRecoveryPacket.ID, UpdatePastureRecoveryPacket::decode, UpdatePastureRecoveryHandler))
// Orientation
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, ClientboundUpdateOrientationHandler))
@@ -591,6 +599,7 @@ object CobblemonNetwork {
// Pasture
list.add(PacketRegisterInfo(PasturePokemonPacket.ID, PasturePokemonPacket::decode, PasturePokemonHandler))
list.add(PacketRegisterInfo(AdjustPastureRangePacket.ID, AdjustPastureRangePacket::decode, AdjustPastureRangeHandler))
list.add(PacketRegisterInfo(UnpasturePokemonPacket.ID, UnpasturePokemonPacket::decode, UnpasturePokemonHandler))
list.add(PacketRegisterInfo(UnpastureAllPokemonPacket.ID, UnpastureAllPokemonPacket::decode, UnpastureAllPokemonHandler))
list.add(PacketRegisterInfo(SetPastureConflictPacket.ID, SetPastureConflictPacket::decode, SetPastureConflictHandler))
@@ -657,4 +666,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -15,11 +15,13 @@ import com.cobblemon.mod.common.api.ai.asVariables
import com.cobblemon.mod.common.api.ai.config.task.WanderTaskConfig.Companion.WANDER
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.withQueryValue
import com.mojang.datafixers.util.Either
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.behavior.BehaviorControl
@@ -69,7 +71,14 @@ class AirWanderTaskConfig : SingleTaskConfig {
val rotVec = entity.getViewVector(0F)
pathCooldown.setWithExpiry(true, 60L)
val target = HoverRandomPos.getPos(entity, horizontalRange.resolveInt(runtime), verticalRange.resolveInt(runtime), rotVec.x, rotVec.y, 1.5707964f, minUpwardsMovement.resolveInt(runtime), minDownwardsMovement.resolveInt(runtime))
val tethering = (entity as? PokemonEntity)?.tethering
val horizontal = horizontalRange.resolveInt(runtime)
val vertical = verticalRange.resolveInt(runtime)
val minUpwards = minUpwardsMovement.resolveInt(runtime)
val minDownwards = minDownwardsMovement.resolveInt(runtime)
val target = (0 until 10).asSequence()
.mapNotNull { HoverRandomPos.getPos(entity, horizontal, vertical, rotVec.x, rotVec.y, 1.5707964f, minUpwards, minDownwards) }
.firstOrNull { tethering?.canRoamTo(BlockPos.containing(it)) != false }
?: return@Trigger false
walkTarget.set(WalkTarget(target, speedMultiplier.resolveFloat(runtime), 3))
lookTarget.set(BlockPosTracker(target))
@@ -78,4 +87,4 @@ class AirWanderTaskConfig : SingleTaskConfig {
}
}
}
}
}
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.ai.config.task.WanderTaskConfig.Companion.WA
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangValue
import com.cobblemon.mod.common.api.npc.configuration.MoLangConfigVariable
import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
@@ -95,6 +96,7 @@ class WaterWanderTaskConfig : SingleTaskConfig {
attempts++
target = BehaviorUtils.getRandomSwimmablePos(entity, horizontalRange.resolveInt(mainThreadRuntime), verticalRange.resolveInt(mainThreadRuntime))
?: continue
if ((entity as? PokemonEntity)?.tethering?.canRoamTo(BlockPos.containing(target)) == false) continue
pos = BlockPos.containing(target).takeIf(wanderControl::isSuitable)
}
@@ -143,4 +145,4 @@ class WaterWanderTaskConfig : SingleTaskConfig {
}
}
}
}
}
@@ -34,7 +34,7 @@ object PastureLinkManager {
fun getLinkByPlayer(player: ServerPlayer): PastureLink? {
val link = getLinkByPlayerId(player.uuid)
if (link != null) {
if (!player.level().dimensionTypeRegistration().`is`(link.dimension) || !link.pos.closerToCenterThan(player.position(), 10.0)) {
if (player.level().dimension().location() != link.dimension || !link.pos.closerToCenterThan(player.position(), 10.0)) {
links.remove(player.uuid)
return null
}
@@ -46,8 +46,8 @@ object PastureLinkManager {
@JvmStatic
fun removeAt(world: ServerLevel, pos: BlockPos) {
links.removeIf { (uuid, pastureLink) ->
val shouldRemove = world.dimensionTypeRegistration().`is`(pastureLink.dimension) && pastureLink.pos == pos
uuid.getPlayer()?.sendPacket(ClosePasturePacket())
val shouldRemove = world.dimension().location() == pastureLink.dimension && pastureLink.pos == pos
if (shouldRemove) uuid.getPlayer()?.sendPacket(ClosePasturePacket())
return@removeIf shouldRemove
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.pasture
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.network.RegistryFriendlyByteBuf
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor
data class PastureRange(val north: Int, val east: Int, val south: Int, val west: Int) {
fun isValid(maximum: Int): Boolean =
listOf(north, east, south, west).all { it in 0..maximum } &&
north.toLong() + south >= 8 && east.toLong() + west >= 8
operator fun get(direction: Direction): Int = when (direction) {
Direction.NORTH -> north
Direction.EAST -> east
Direction.SOUTH -> south
Direction.WEST -> west
else -> error("Pasture range directions must be horizontal")
}
fun adjusted(direction: Direction, amount: Int): PastureRange = when (direction) {
Direction.NORTH -> copy(north = north + amount)
Direction.EAST -> copy(east = east + amount)
Direction.SOUTH -> copy(south = south + amount)
Direction.WEST -> copy(west = west + amount)
else -> this
}
fun minPos(origin: BlockPos) = origin.offset(-west, 0, -north)
fun maxPos(origin: BlockPos) = origin.offset(east, 0, south)
fun nearbyBlocks(origin: BlockPos, preferred: BlockPos, width: Double): Sequence<BlockPos> = sequence {
val halfWidth = width / 2.0
val minX = ceil(origin.x - west + halfWidth - 0.5).toInt()
val maxX = floor(origin.x + east - halfWidth - 0.5).toInt()
val minZ = ceil(origin.z - north + halfWidth - 0.5).toInt()
val maxZ = floor(origin.z + south - halfWidth - 0.5).toInt()
if (minX > maxX || minZ > maxZ) return@sequence
val centerX = preferred.x.coerceIn(minX, maxX)
val centerZ = preferred.z.coerceIn(minZ, maxZ)
for (distance in 0..16) {
for (dx in -distance..distance) {
val x = centerX + dx
if (x !in minX..maxX) continue
val dz = distance - abs(dx)
val z = centerZ + dz
if (z in minZ..maxZ) yield(BlockPos(x, origin.y, z))
if (dz != 0 && centerZ - dz in minZ..maxZ) yield(BlockPos(x, origin.y, centerZ - dz))
}
}
}
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeVarInt(north)
buffer.writeVarInt(east)
buffer.writeVarInt(south)
buffer.writeVarInt(west)
}
companion object {
const val MAX_DISTANCE = 64
fun fromBounds(origin: BlockPos, min: BlockPos, max: BlockPos) = PastureRange(
origin.z - min.z, max.x - origin.x, max.z - origin.z, origin.x - min.x
)
fun decode(buffer: RegistryFriendlyByteBuf) = PastureRange(
buffer.readVarInt(), buffer.readVarInt(), buffer.readVarInt(), buffer.readVarInt()
)
}
}
@@ -25,7 +25,6 @@ import com.cobblemon.mod.common.util.rotateShape
import com.mojang.serialization.MapCodec
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.StringRepresentable
@@ -283,11 +282,13 @@ class PastureBlock(settings: Properties): BaseEntityBlock(settings), SimpleWater
pastureId = linkId,
permissions = perms,
limit = baseEntity.getMaxTethered(),
tetheredPokemon = baseEntity.tetheredPokemon.mapNotNull { it.toDTO(player) }
tetheredPokemon = baseEntity.tetheredPokemon.mapNotNull { it.toDTO(player) },
range = baseEntity.getRange(),
facing = state.getValue(HorizontalDirectionalBlock.FACING)
)
)
PastureLinkManager.createLink(player.uuid, PastureLink(linkId, pcId, ResourceLocation.tryParse(world.dimensionTypeRegistration().registeredName)!!, getBasePosition(state, pos), perms))
PastureLinkManager.createLink(player.uuid, PastureLink(linkId, pcId, world.dimension().location(), getBasePosition(state, pos), perms))
world.playSoundServer(
position = pos.bottomCenter,
@@ -366,4 +367,4 @@ class PastureBlock(settings: Properties): BaseEntityBlock(settings), SimpleWater
return state
}
}
}
@@ -14,15 +14,19 @@ import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.advancement.CobblemonCriteria
import com.cobblemon.mod.common.api.pasture.PastureLinkManager
import com.cobblemon.mod.common.api.pasture.PastureRange
import com.cobblemon.mod.common.api.scheduling.afterOnServer
import com.cobblemon.mod.common.api.text.italicise
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.block.PastureBlock
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonBehaviourFlag
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.net.messages.client.pasture.ClosePasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.OpenPasturePacket
import com.cobblemon.mod.common.net.messages.client.pasture.PokemonPasturedPacket
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureRecoveryPacket
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureConflictFlagPacket
import com.cobblemon.mod.common.net.serverhandling.storage.SendOutPokemonHandler
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.DataKeys
@@ -39,7 +43,6 @@ import net.minecraft.core.Vec3i
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.NbtUtils
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.tags.FluidTags
@@ -48,38 +51,61 @@ import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.HorizontalDirectionalBlock
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.entity.EntityTypeTest
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
BlockEntity(CobblemonBlockEntities.PASTURE, pos, state) {
open class Tethering(
val minRoamPos: BlockPos,
val maxRoamPos: BlockPos,
var minRoamPos: BlockPos,
var maxRoamPos: BlockPos,
val playerId: UUID,
val playerName: String,
val tetheringId: UUID,
val pokemonId: UUID,
val pcId: UUID,
val entityId: Int,
val pasturePos: BlockPos
var entityId: Int,
val pasturePos: BlockPos,
var entityUuid: UUID? = null,
var lastEntityPos: BlockPos? = null,
var respawnAt: Long = 0L,
var healingAfterFaint: Boolean = false,
var conflictEnabled: Boolean? = null
) {
fun getPokemon(): Pokemon? {
val srv = server() ?: return null
val pc = Cobblemon.storage.getPC(pcId, srv.registryAccess())
return pc[pokemonId]
}
val box = AABB(minRoamPos.toVec3d(), maxRoamPos.toVec3d())
open fun canRoamTo(pos: BlockPos) = box.contains(pos.center)
val box get() = AABB(minRoamPos.toVec3d(), maxRoamPos.toVec3d())
fun canRoamTo(x: Double, z: Double) = x >= minRoamPos.x && x < maxRoamPos.x && z >= minRoamPos.z && z < maxRoamPos.z
open fun canRoamTo(pos: BlockPos) = canRoamTo(pos.x + 0.5, pos.z + 0.5)
internal fun beginRecovery(now: Long, fainted: Boolean) {
if (fainted && !healingAfterFaint) {
healingAfterFaint = true
respawnAt = now + FAINT_COOLDOWN
} else if (respawnAt == 0L) {
respawnAt = now + MISSING_ENTITY_COOLDOWN
}
}
fun toDTO(player: ServerPlayer): OpenPasturePacket.PasturePokemonDataDTO? {
val pokemon = getPokemon() ?: return null
val entity = pokemon.entity
val behaviourFlags = entity?.getActiveBehaviourFlags() ?: emptySet()
val behaviourFlags = (entity?.getActiveBehaviourFlags() ?: emptySet()).toMutableSet()
when (conflictEnabled) {
true -> behaviourFlags.add(PokemonBehaviourFlag.PASTURE_CONFLICT)
false -> behaviourFlags.remove(PokemonBehaviourFlag.PASTURE_CONFLICT)
null -> Unit
}
return OpenPasturePacket.PasturePokemonDataDTO(
pokemonId = pokemonId,
@@ -91,12 +117,16 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
heldItem = pokemon.heldItem(),
level = pokemon.level,
entityKnown = (entity?.tethering?.tetheringId == tetheringId),
behaviourFlags = behaviourFlags
behaviourFlags = behaviourFlags,
healingAfterFaint = healingAfterFaint,
respawnAt = respawnAt
)
}
}
companion object {
private const val FAINT_COOLDOWN = 24000L
private const val MISSING_ENTITY_COOLDOWN = 200L
internal val TICKER = BlockEntityTicker<PokemonPastureBlockEntity> { world, _, _, blockEntity ->
if (world.isClientSide) return@BlockEntityTicker
blockEntity.ticksUntilCheck--
@@ -134,6 +164,26 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
fun getMaxTethered() = Cobblemon.config.defaultPasturedPokemonLimit
fun getRange() = PastureRange.fromBounds(blockPos, minRoamPos, maxRoamPos)
fun setRange(range: PastureRange) {
minRoamPos = range.minPos(blockPos).atY(minRoamPos.y)
maxRoamPos = range.maxPos(blockPos).atY(maxRoamPos.y)
tetheredPokemon.forEach { tethering ->
tethering.minRoamPos = minRoamPos
tethering.maxRoamPos = maxRoamPos
tethering.getPokemon()?.entity?.let { entity ->
if (entity.tethering?.tetheringId == tethering.tetheringId) {
entity.tethering = tethering
entity.navigation.stop()
entity.brain.eraseMemory(MemoryModuleType.WALK_TARGET)
entity.checkPastureTether()
}
}
}
setChanged()
}
fun canAddPokemon(player: ServerPlayer, pokemon: Pokemon, maxPerPlayer: Int): Boolean {
val forThisPlayer = tetheredPokemon.count { it.playerId == player.uuid }
// Shouldn't be possible, client should've prevented it
@@ -158,57 +208,64 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
fun tether(player: ServerPlayer, pokemon: Pokemon, directionToBehind: Direction): Boolean {
val tethering = Tethering(
minRoamPos, maxRoamPos, player.uuid, player.gameProfile.name, UUID.randomUUID(),
pokemon.uuid, Cobblemon.storage.getPC(player).uuid, -1, blockPos
)
if (!spawnPokemon(pokemon, tethering, directionToBehind)) return false
pokemon.tetheringId = tethering.tetheringId
pokemon.onChange()
tetheredPokemon.add(tethering)
tethering.toDTO(player)?.let { player.sendPacket(PokemonPasturedPacket(it)) }
setChanged()
CobblemonCriteria.PASTURE_USE.trigger(player, pokemon)
return true
}
private fun spawnPokemon(pokemon: Pokemon, tethering: Tethering, directionToBehind: Direction): Boolean {
val world = level ?: return false
val entity = PokemonEntity(world, pokemon = pokemon)
entity.refreshDimensions()
val width = entity.boundingBox.xsize
val idealPlace = blockPos.offset(directionToBehind.normal.multiply(ceil(width).toInt() + 1))
var box = entity.getDimensions(Pose.STANDING).makeBoundingBox(idealPlace.center.subtract(0.0, 0.5, 0.0))
for (i in 0..5) {
box = box.move(directionToBehind.normal.x.toDouble(), 0.0, directionToBehind.normal.z.toDouble())
val fixedPosition = makeSuitableY(world, idealPlace.offset(directionToBehind.normal.multiply(i + 1)), entity, box)
if (fixedPosition != null) {
entity.setPos(fixedPosition.center.subtract(0.0, 0.5, 0.0))
val pc = Cobblemon.storage.getPC(player)
entity.beamMode = 2
afterOnServer(seconds = 0.5F) {
if(entity.pokemon.shiny) {
SpawnSnowstormEntityParticlePacket(cobblemonResource("shiny_ring"), entity.id, listOf("shiny_particles", "middle"))
.sendToPlayersAround(entity.x, entity.y, entity.z, 64.0, entity.level().dimension())
}
entity.after(seconds = SendOutPokemonHandler.SEND_OUT_DURATION) {
entity.beamMode = 0
}
}
if (world.addFreshEntity(entity)) {
val tethering = Tethering(
minRoamPos = minRoamPos,
maxRoamPos = maxRoamPos,
playerId = player.uuid,
playerName = player.gameProfile.name,
tetheringId = UUID.randomUUID(),
pokemonId = pokemon.uuid,
pcId = pc.uuid,
entityId = entity.id,
pasturePos = blockPos
)
pokemon.tetheringId = tethering.tetheringId
tetheredPokemon.add(tethering)
entity.tethering = tethering
tethering.toDTO(player)?.let { player.sendPacket(PokemonPasturedPacket(it)) }
setChanged()
CobblemonCriteria.PASTURE_USE.trigger(player, pokemon)
return true
} else {
Cobblemon.LOGGER.warn("Couldn't spawn pastured Pokémon for some reason")
}
break
val position = findSpawnPosition(entity, tethering.lastEntityPos, directionToBehind) ?: return false
entity.setPos(position)
entity.beamMode = 2
entity.tethering = tethering
entity.setBehaviourFlag(PokemonBehaviourFlag.PASTURE_CONFLICT, tethering.conflictEnabled == true)
if (!world.addFreshEntity(entity)) return false
if (tethering.conflictEnabled == true) entity.remakeBrain()
tethering.entityId = entity.id
tethering.entityUuid = entity.uuid
tethering.lastEntityPos = entity.blockPosition()
tethering.respawnAt = 0L
syncRecovery(tethering)
afterOnServer(seconds = 0.5F) {
if (!entity.isRemoved && entity.pokemon.shiny) {
SpawnSnowstormEntityParticlePacket(cobblemonResource("shiny_ring"), entity.id, listOf("shiny_particles", "middle"))
.sendToPlayersAround(entity.x, entity.y, entity.z, 64.0, entity.level().dimension())
}
entity.after(seconds = SendOutPokemonHandler.SEND_OUT_DURATION) {
entity.beamMode = 0
}
}
return true
}
return false
fun findSpawnPosition(entity: PokemonEntity, preferred: BlockPos? = null, direction: Direction = blockState.getValue(HorizontalDirectionalBlock.FACING)): Vec3? {
val world = level ?: return null
val dimensions = entity.getDimensions(Pose.STANDING)
val idealPlace = preferred ?: blockPos.offset(direction.normal.multiply(ceil(dimensions.width / 2.0).toInt() + 1))
for (candidate in getRange().nearbyBlocks(blockPos, idealPlace, dimensions.width.toDouble())) {
if (!world.hasChunkAt(candidate)) continue
val box = dimensions.makeBoundingBox(candidate.center.subtract(0.0, 0.5, 0.0))
val fixedPosition = makeSuitableY(world, candidate, entity, box) ?: continue
val position = fixedPosition.center.subtract(0.0, 0.5, 0.0)
val spawnBox = dimensions.makeBoundingBox(position)
if (world.noCollision(entity, spawnBox) &&
world.getEntitiesOfClass(PokemonEntity::class.java, spawnBox) { it != entity }.isEmpty()) {
return position
}
}
return null
}
private fun togglePastureOn(on: Boolean) {
@@ -289,6 +346,7 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
}
fun checkPokemon() {
val world = level as? ServerLevel ?: return
val deadLinks = mutableListOf<UUID>()
tetheredPokemon.forEach {
val pokemon = it.getPokemon()
@@ -296,6 +354,8 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
deadLinks.add(it.pokemonId)
} else if (pokemon.tetheringId == null || pokemon.tetheringId != it.tetheringId) {
deadLinks.add(it.pokemonId)
} else {
checkDeployment(world, it, pokemon)
}
}
deadLinks.forEach(::releasePokemon)
@@ -303,6 +363,85 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
setChanged()
}
private fun checkDeployment(world: ServerLevel, tethering: Tethering, pokemon: Pokemon) {
val now = world.gameTime
if (pokemon.isFainted() && !tethering.healingAfterFaint) {
tethering.beginRecovery(now, fainted = true)
syncRecovery(tethering)
}
val entity = (pokemon.entity ?: tethering.entityUuid?.let(world::getEntity) as? PokemonEntity)
?.takeIf { !it.isRemoved && it.tethering?.tetheringId == tethering.tetheringId }
if (tethering.conflictEnabled == null && entity != null) {
tethering.conflictEnabled = entity.getBehaviourFlag(PokemonBehaviourFlag.PASTURE_CONFLICT)
}
if (entity != null && pokemon.isFainted()) entity.discard()
if (entity != null && !entity.isRemoved) {
if (tethering.conflictEnabled != null && entity.getBehaviourFlag(PokemonBehaviourFlag.PASTURE_CONFLICT) != tethering.conflictEnabled) {
entity.setBehaviourFlag(PokemonBehaviourFlag.PASTURE_CONFLICT, tethering.conflictEnabled == true)
entity.remakeBrain()
}
tethering.entityUuid = entity.uuid
tethering.lastEntityPos = entity.blockPosition()
entity.tethering = tethering
if (tethering.respawnAt != 0L || tethering.healingAfterFaint) {
tethering.respawnAt = 0L
tethering.healingAfterFaint = false
syncRecovery(tethering)
}
return
}
// A Pokémon in an unloaded chunk is still deployed. Never create its replacement.
val lastPosition = tethering.lastEntityPos
if (lastPosition != null) {
if (!world.hasChunkAt(lastPosition)) return
}
// The last saved position may predate a chunk crossing. Require the entire range
// to be entity-ticking before deciding that its missing Pokémon has disappeared.
for (chunkX in (minRoamPos.x shr 4)..(maxRoamPos.x shr 4)) {
for (chunkZ in (minRoamPos.z shr 4)..(maxRoamPos.z shr 4)) {
if (!world.isPositionEntityTicking(BlockPos(chunkX shl 4, blockPos.y, chunkZ shl 4))) return
}
}
if (tethering.respawnAt == 0L) {
tethering.beginRecovery(now, fainted = false)
syncRecovery(tethering)
return
}
if (now < tethering.respawnAt) return
if (tethering.healingAfterFaint) {
pokemon.heal()
if (pokemon.isFainted()) return
tethering.healingAfterFaint = false
syncRecovery(tethering)
}
val radius = Cobblemon.config.pastureMaxWanderDistance.toDouble()
val nearby = world.getEntitiesOfClass(PokemonEntity::class.java, AABB.ofSize(blockPos.toVec3d(), radius * 2, world.height.toDouble(), radius * 2)).size
val chunkDiameter = (radius / 16) * 2
if (nearby >= Cobblemon.config.pastureMaxPerChunk * chunkDiameter * chunkDiameter ||
!spawnPokemon(pokemon, tethering, blockState.getValue(HorizontalDirectionalBlock.FACING))) {
tethering.respawnAt = now + MISSING_ENTITY_COOLDOWN
syncRecovery(tethering)
}
}
private fun syncRecovery(tethering: Tethering) {
val world = level as? ServerLevel ?: return
val packet = UpdatePastureRecoveryPacket(tethering.pokemonId, tethering.healingAfterFaint, tethering.respawnAt)
world.server.playerList.players.filter(::isPlayerViewing).forEach { it.sendPacket(packet) }
}
fun setConflict(tethering: Tethering, enabled: Boolean) {
tethering.conflictEnabled = enabled
tethering.getPokemon()?.entity?.takeIf { it.tethering?.tetheringId == tethering.tetheringId }?.let {
it.setBehaviourFlag(PokemonBehaviourFlag.PASTURE_CONFLICT, enabled)
it.remakeBrain()
}
setChanged()
val world = level as? ServerLevel ?: return
val packet = UpdatePastureConflictFlagPacket(tethering.pokemonId, enabled)
world.server.playerList.players.filter(::isPlayerViewing).forEach { it.sendPacket(packet) }
}
fun onBroken() {
if (level is ServerLevel) {
tetheredPokemon.toList().forEach { releasePokemon(it.pokemonId) }
@@ -312,7 +451,10 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
fun releasePokemon(pokemonId: UUID) {
val tethering = tetheredPokemon.find { it.pokemonId == pokemonId } ?: return
tethering.getPokemon()?.tetheringId = null
tethering.getPokemon()?.let { pokemon ->
pokemon.tetheringId = null
pokemon.onChange()
}
tetheredPokemon.remove(tethering)
setChanged()
}
@@ -320,7 +462,10 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
fun releaseAllPokemon(playerId: UUID): List<UUID> {
val unpastured = mutableListOf<UUID>()
tetheredPokemon.filter { it.playerId == playerId }.forEach {
it.getPokemon()?.tetheringId = null
it.getPokemon()?.let { pokemon ->
pokemon.tetheringId = null
pokemon.onChange()
}
tetheredPokemon.remove(it)
unpastured.add(it.pokemonId)
}
@@ -343,9 +488,7 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
private fun isPlayerViewing(player: ServerPlayer): Boolean {
val pastureLink = PastureLinkManager.getLinkByPlayer(player)
return pastureLink != null && pastureLink.pos == blockPos && pastureLink.dimension == ResourceLocation.tryParse(
player.level().dimensionTypeRegistration().registeredName // todo (techdaan): confirm this is good
)
return pastureLink != null && pastureLink.pos == blockPos && pastureLink.dimension == player.level().dimension().location()
}
override fun loadAdditional(nbt: CompoundTag, registryLookup: HolderLookup.Provider) {
@@ -353,6 +496,9 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
val list = nbt.getList(DataKeys.TETHER_POKEMON, CompoundTag.TAG_COMPOUND.toInt())
this.ownerId = if (nbt.hasUUID(DataKeys.TETHER_OWNER_ID)) nbt.getUUID(DataKeys.TETHER_OWNER_ID) else null
this.ownerName = nbt.getString(DataKeys.TETHER_OWNER_NAME).takeIf { it.isNotEmpty() } ?: ""
this.minRoamPos = NbtUtils.readBlockPos(nbt, DataKeys.TETHER_MIN_ROAM_POS).orElse(minRoamPos)
this.maxRoamPos = NbtUtils.readBlockPos(nbt, DataKeys.TETHER_MAX_ROAM_POS).orElse(maxRoamPos)
tetheredPokemon.clear()
for (tetheringNBT in list) {
tetheringNBT as CompoundTag
val tetheringId = tetheringNBT.getUUID(DataKeys.TETHERING_ID)
@@ -365,17 +511,20 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
minRoamPos = minRoamPos,
maxRoamPos = maxRoamPos,
playerId = playerId,
playerName = ownerName,
playerName = tetheringNBT.getString("PlayerName").ifEmpty { ownerName },
tetheringId = tetheringId,
pokemonId = pokemonId,
pcId = pcId,
entityId = entityId,
pasturePos = blockPos
pasturePos = blockPos,
entityUuid = if (tetheringNBT.hasUUID("EntityUUID")) tetheringNBT.getUUID("EntityUUID") else null,
lastEntityPos = NbtUtils.readBlockPos(tetheringNBT, "LastEntityPos").orElse(null),
respawnAt = tetheringNBT.getLong("RespawnAt"),
healingAfterFaint = tetheringNBT.getBoolean("HealingAfterFaint"),
conflictEnabled = if (tetheringNBT.contains("PastureConflict")) tetheringNBT.getBoolean("PastureConflict") else null
)
)
}
this.minRoamPos = NbtUtils.readBlockPos(nbt, DataKeys.TETHER_MIN_ROAM_POS).get()
this.maxRoamPos = NbtUtils.readBlockPos(nbt, DataKeys.TETHER_MAX_ROAM_POS).get()
}
override fun saveAdditional(nbt: CompoundTag, registryLookup: HolderLookup.Provider) {
@@ -388,6 +537,12 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
tetheringNBT.putUUID(DataKeys.POKEMON_UUID, tethering.pokemonId)
tetheringNBT.putUUID(DataKeys.PC_ID, tethering.pcId)
tetheringNBT.putInt(DataKeys.TETHERING_ENTITY_ID, tethering.entityId)
tetheringNBT.putString("PlayerName", tethering.playerName)
tethering.entityUuid?.let { tetheringNBT.putUUID("EntityUUID", it) }
tethering.lastEntityPos?.let { tetheringNBT.put("LastEntityPos", NbtUtils.writeBlockPos(it)) }
tetheringNBT.putLong("RespawnAt", tethering.respawnAt)
tetheringNBT.putBoolean("HealingAfterFaint", tethering.healingAfterFaint)
tethering.conflictEnabled?.let { tetheringNBT.putBoolean("PastureConflict", it) }
list.add(tetheringNBT)
}
nbt.put(DataKeys.TETHER_POKEMON, list)
@@ -396,4 +551,4 @@ class PokemonPastureBlockEntity(pos: BlockPos, state: BlockState) :
ownerId?.let { nbt.putUUID(DataKeys.TETHER_OWNER_ID, it) }
nbt.putString(DataKeys.TETHER_OWNER_NAME, this.ownerName)
}
}
}
@@ -11,6 +11,8 @@ package com.cobblemon.mod.common.client.gui.pasture
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.pasture.PasturePermissions
import com.cobblemon.mod.common.api.pasture.PastureRange
import net.minecraft.core.Direction
import com.cobblemon.mod.common.api.reactive.SettableObservable
import com.cobblemon.mod.common.client.gui.pc.PCGUIConfiguration
import com.cobblemon.mod.common.net.messages.client.pasture.OpenPasturePacket
@@ -21,7 +23,9 @@ class PasturePCGUIConfiguration(
val pastureId: UUID,
val limit: Int,
val pasturedPokemon: SettableObservable<List<OpenPasturePacket.PasturePokemonDataDTO>>,
var permissions: PasturePermissions
var permissions: PasturePermissions,
var range: PastureRange,
val facing: Direction
) : PCGUIConfiguration(
exitFunction = { it.closeNormally(unlink = true) },
showParty = false,
@@ -32,4 +36,6 @@ class PasturePCGUIConfiguration(
}
},
canSelect = { pokemon -> !pokemon.isFainted() && pokemon.tetheringId == null }
)
) {
var editingRange = false
}
@@ -58,6 +58,7 @@ class PasturePokemonScrollList(
private val scrollOverlayResource = cobblemonResource("textures/gui/pasture/pasture_scroll_overlay.png")
private val slotResource = cobblemonResource("textures/gui/pasture/pasture_slot.png")
private val slotOwnerResource = cobblemonResource("textures/gui/pasture/pasture_slot_owner.png")
private val slotFaintedResource = cobblemonResource("textures/gui/pasture/pasture_slot_fainted.png")
}
private var scrolling = false
@@ -194,7 +195,7 @@ class PasturePokemonScrollList(
}
)
override fun getNarration() = pokemon.displayName
override fun getNarration() = if (pokemon.healingAfterFaint) lang("ui.pasture.fainted").append(" ").append(pokemon.displayName) else pokemon.displayName
override fun render(
context: GuiGraphics,
@@ -214,7 +215,7 @@ class PasturePokemonScrollList(
val matrixStack = context.pose()
blitk(
matrixStack = matrixStack,
texture = if (isOwned()) slotOwnerResource else slotResource,
texture = if (pokemon.healingAfterFaint) slotFaintedResource else if (isOwned()) slotOwnerResource else slotResource,
x = x,
y = y,
height = SLOT_HEIGHT,
@@ -234,7 +235,8 @@ class PasturePokemonScrollList(
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
partialTicks = partialTicks,
scale = 4.5F
scale = 4.5F,
a = if (pokemon.healingAfterFaint) 0.7F else 1F
)
matrixStack.popPose()
@@ -249,14 +251,21 @@ class PasturePokemonScrollList(
)
}
drawScaledTextJustifiedRight(
context = context,
text = lang("ui.lv.number", pokemon.level),
x = x + 59,
y = y + 17,
shadow = true,
scale = SCALE
)
val recovering = pokemon.healingAfterFaint || pokemon.respawnAt > 0L
if (recovering) {
if (!pokemon.healingAfterFaint) {
drawScaledText(context, text = lang("ui.pasture.returning"),
x = x + 18, y = y + 7, scale = SCALE, colour = 0xFFFF8585.toInt(), shadow = true)
}
val remainingTicks = (pokemon.respawnAt - (client.level?.gameTime ?: 0L)).coerceAtLeast(0L)
val seconds = (remainingTicks + 19L) / 20L
val timer = if (remainingTicks == 0L) lang("ui.pasture.ready") else "%d:%02d".format(seconds / 60L, seconds % 60L).text()
drawScaledTextJustifiedRight(context = context, text = timer, x = x + 59, y = y + 17,
shadow = true, scale = SCALE)
} else {
drawScaledTextJustifiedRight(context = context, text = lang("ui.lv.number", pokemon.level),
x = x + 59, y = y + 17, shadow = true, scale = SCALE)
}
drawScaledText(
context = context,
@@ -279,7 +288,7 @@ class PasturePokemonScrollList(
}
if (canUnpasture()) {
if (canDefend() == true) {
if (isOwned() && canDefend() == true) {
conflictButton.setPos(x + 44, y + 3)
conflictButton.render(context, mouseX, mouseY, partialTicks)
}
@@ -291,7 +300,7 @@ class PasturePokemonScrollList(
override fun mouseClicked(mouseX: Double, mouseY: Double, delta: Int): Boolean {
if (canUnpasture()) {
if (canDefend() == true && conflictButton.isHovered(mouseX, mouseY)) {
if (isOwned() && canDefend() == true && conflictButton.isHovered(mouseX, mouseY)) {
conflictButton.onPress()
return true
}
@@ -303,4 +312,4 @@ class PasturePokemonScrollList(
return false
}
}
}
}
@@ -0,0 +1,225 @@
/*
* 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.pasture
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.font
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.pc.StorageWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.gui.PCBoxWallpaperRepository
import com.cobblemon.mod.common.net.messages.server.pasture.AdjustPastureRangePacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.core.Direction
import net.minecraft.network.chat.Component
class PastureRangeWidget(x: Int, y: Int, private val configuration: PasturePCGUIConfiguration) :
SoundlessWidget(x, y, StorageWidget.SCREEN_WIDTH, StorageWidget.SCREEN_HEIGHT, lang("ui.pasture.range")) {
private val rangeResource = cobblemonResource("textures/gui/pasture/pasture_range.png")
private val glowResource = cobblemonResource("textures/gui/pc/wallpaper/basic/glow/wallpaper_basic_05.png")
private val overlayResource = cobblemonResource("textures/gui/pc/pc_screen_overlay.png")
private val infoResource = cobblemonResource("textures/gui/pasture/pasture_range_help.png")
private val infoButtonResource = cobblemonResource("textures/gui/pasture/pasture_button.png")
private val infoButtonSize = 17
private val directions = listOf(configuration.facing, configuration.facing.clockWise, configuration.facing.opposite, configuration.facing.counterClockWise)
private val arrowBounds = listOf(
intArrayOf(75, 17, 103, 46), intArrayOf(116, 64, 152, 96),
intArrayOf(75, 109, 103, 143), intArrayOf(26, 64, 62, 96)
)
// Centers of the numeral shadows in pasture_range.png, relative to its (85, 27) panel origin.
private val numberPositions = listOf(89 to 37, 125 to 77, 89 to 114, 53 to 77)
private var hoveredDirection: Direction? = null
private var showInfo = false
val isInfoOpen get() = showInfo
fun closeInfo() { showInfo = false }
private val infoButtonX get() = x + 5
private val infoButtonY get() = y + 5
private val infoPanelX get() = x + 7
private val infoPanelY get() = y + 40
private val infoPanelWidth = 160
private val infoPanelHeight = 74
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
blitk(
matrixStack = context.pose(), texture = PCBoxWallpaperRepository.defaultWallpaper,
x = x, y = y, width = width, height = height
)
blitk(
matrixStack = context.pose(), texture = rangeResource,
x = x + 3, y = y + 3, width = width - 6, height = height - 6,
uOffset = 88, vOffset = 30, textureWidth = 349, textureHeight = 205
)
blitk(
matrixStack = context.pose(), texture = glowResource,
x = x - 17, y = y - 17, width = 208, height = 189
)
blitk(matrixStack = context.pose(), texture = overlayResource, x = x, y = y, width = width, height = height)
hoveredDirection = if (showInfo) null else directionAt(mouseX.toDouble(), mouseY.toDouble())
directions.indexOf(hoveredDirection).takeIf { it >= 0 }?.let { index ->
renderArrowHighlight(context, index)
}
context.pose().pushPose()
context.pose().translate(-0.5F, 0F, 0F)
directions.forEachIndexed { index, direction ->
val (numberX, numberY) = numberPositions[index]
drawScaledText(context, text = Component.literal(configuration.range[direction].toString()),
x = x + numberX + 1, y = y + numberY - 7, centered = true,
colour = 0xFFFFFFFF.toInt())
}
context.pose().popPose()
if (showInfo) context.fill(x + 3, y + 3, x + width - 3, y + height - 3, 0xAA002730.toInt())
val infoHovered = isOverInfoButton(mouseX.toDouble(), mouseY.toDouble())
// Use the same button face and persistent open-state highlight as the gear.
blitk(matrixStack = context.pose(), texture = infoButtonResource,
x = infoButtonX, y = infoButtonY, width = 13, height = infoButtonSize,
textureWidth = 70, textureHeight = 34)
blitk(matrixStack = context.pose(), texture = infoButtonResource,
x = infoButtonX + 13, y = infoButtonY, width = 4, height = infoButtonSize,
uOffset = 66, textureWidth = 70, textureHeight = 34)
if (showInfo || infoHovered) context.fill(infoButtonX + 2, infoButtonY + 2,
infoButtonX + 16, infoButtonY + 15, 0x88FFFFFF.toInt())
blitk(matrixStack = context.pose(), texture = infoResource,
x = infoButtonX + 4.5F, y = infoButtonY + 3.5F, width = 8, height = 10)
if (showInfo) {
val options = Minecraft.getInstance().options
val boxX = infoPanelX
val boxY = infoPanelY
val boxWidth = infoPanelWidth
val boxHeight = infoPanelHeight
context.fill(boxX + 1, boxY, boxX + boxWidth - 1, boxY + boxHeight, 0x96FFFFFF.toInt())
context.fill(boxX, boxY + 1, boxX + boxWidth, boxY + boxHeight - 1, 0x96FFFFFF.toInt())
blitk(matrixStack = context.pose(), texture = PCBoxWallpaperRepository.defaultWallpaper,
x = boxX + 1, y = boxY + 1, width = boxWidth - 2, height = boxHeight - 2,
textureWidth = StorageWidget.SCREEN_WIDTH, textureHeight = StorageWidget.SCREEN_HEIGHT)
val increaseBinding = if (options.keyAttack.matchesMouse(0)) lang("ui.pasture.range.left_click")
else options.keyAttack.translatedKeyMessage.copy()
val decreaseBinding = if (options.keyUse.matchesMouse(1)) lang("ui.pasture.range.right_click")
else options.keyUse.translatedKeyMessage.copy()
val columns = listOf(
Triple(boxX + 43, lang("ui.pasture.range.increase").bold(), listOf(increaseBinding, lang("ui.pasture.range.scroll_up"))),
Triple(boxX + 117, lang("ui.pasture.range.decrease").bold(), listOf(decreaseBinding, lang("ui.pasture.range.scroll_down")))
)
val detailWidth = columns.flatMap { it.third }.maxOf {
Minecraft.getInstance().font.width(it.copy().font(CobblemonResources.DEFAULT_LARGE))
}
val detailScale = minOf(1F, 72F / detailWidth)
drawScaledText(context, font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.pasture.range.help_title").bold(), x = boxX + 80, y = boxY + 8,
centered = true, shadow = true)
context.fill(boxX + 8, boxY + 22, boxX + boxWidth - 8, boxY + 23, 0x96FFFFFF.toInt())
context.fill(boxX + 80, boxY + 27, boxX + 81, boxY + 66, 0x96FFFFFF.toInt())
columns.forEach { (centerX, title, details) ->
drawScaledText(context, font = CobblemonResources.DEFAULT_LARGE,
text = title, x = centerX, y = boxY + 29, centered = true, shadow = true)
details.forEachIndexed { index, line ->
drawScaledText(context, font = CobblemonResources.DEFAULT_LARGE,
text = line, x = centerX, y = boxY + 44 + index * 14,
scale = detailScale, centered = true, shadow = true)
}
}
}
}
private fun renderArrowHighlight(context: GuiGraphics, index: Int) {
fun fill(left: Int, top: Int, right: Int, bottom: Int) {
context.fill(x + left, y + top, x + right, y + bottom, 0x66FFFFFF)
}
// Trace the arrow faces in pasture_range.png; horizontal arrows have longer stems.
// Keep their offset drop shadows visible instead of brightening the surrounding background.
when (index) {
0 -> {
for (row in 0..14) {
val halfWidth = 3 + minOf(row, 11)
fill(89 - halfWidth, 17 + row, 89 + halfWidth, 18 + row)
}
fill(81, 32, 97, 41)
}
1, 3 -> {
for (row in 0..26) {
val tip = 141 + minOf(row, 11, 26 - row)
if (index == 1) fill(137, 64 + row, tip, 65 + row)
else fill(178 - tip, 64 + row, 41, 65 + row)
}
if (index == 1) fill(116, 72, 137, 83)
else fill(41, 72, 62, 83)
}
2 -> {
fill(81, 109, 97, 123)
for (row in 0..14) {
val halfWidth = 14 - maxOf(0, row - 3)
fill(89 - halfWidth, 123 + row, 89 + halfWidth, 124 + row)
}
}
}
}
private fun directionAt(mouseX: Double, mouseY: Double): Direction? {
val index = arrowBounds.indexOfFirst { (left, top, right, bottom) ->
mouseX >= x + left && mouseX < x + right &&
mouseY >= y + top && mouseY < y + bottom
}
return directions.getOrNull(index)
}
private fun adjust(direction: Direction, increase: Boolean): Boolean {
AdjustPastureRangePacket(configuration.pastureId, direction, increase).sendToServer()
return true
}
private fun isOverInfoButton(mouseX: Double, mouseY: Double) =
mouseX >= infoButtonX && mouseX < infoButtonX + infoButtonSize &&
mouseY >= infoButtonY && mouseY < infoButtonY + infoButtonSize
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (pButton == 0 && isOverInfoButton(pMouseX, pMouseY)) {
showInfo = !showInfo
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.PC_CLICK, 1F))
return true
}
if (showInfo) {
if (pMouseX < infoPanelX || pMouseX >= infoPanelX + infoPanelWidth ||
pMouseY < infoPanelY || pMouseY >= infoPanelY + infoPanelHeight) closeInfo()
return true
}
val direction = directionAt(pMouseX, pMouseY) ?: return false
val options = Minecraft.getInstance().options
return when {
options.keyAttack.matchesMouse(pButton) -> adjust(direction, true)
options.keyUse.matchesMouse(pButton) -> adjust(direction, false)
else -> false
}
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
if (showInfo) return true
val direction = directionAt(mouseX, mouseY) ?: return false
if (scrollY == 0.0) return false
return adjust(direction, scrollY > 0)
}
override fun keyPressed(pKeyCode: Int, pScanCode: Int, pModifiers: Int): Boolean {
if (showInfo) return !Minecraft.getInstance().options.keyInventory.matches(pKeyCode, pScanCode)
val direction = hoveredDirection ?: return false
val options = Minecraft.getInstance().options
return when {
options.keyAttack.matches(pKeyCode, pScanCode) -> adjust(direction, true)
options.keyUse.matches(pKeyCode, pScanCode) -> adjust(direction, false)
else -> false
}
}
}
@@ -20,7 +20,9 @@ import com.cobblemon.mod.common.net.messages.server.pasture.UnpastureAllPokemonP
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.sounds.SoundManager
import net.minecraft.network.chat.Component
import net.minecraft.client.gui.components.Button
class PastureWidget(
val storageWidget: StorageWidget,
@@ -32,6 +34,8 @@ class PastureWidget(
companion object {
private val baseResource = cobblemonResource("textures/gui/pasture/pasture_panel.png")
private val buttonResource = cobblemonResource("textures/gui/pasture/pasture_button.png")
private val gearResource = cobblemonResource("textures/gui/pasture/pasture_range_gear.png")
}
private val recallButton = RecallButton(
@@ -42,6 +46,14 @@ class PastureWidget(
UnpastureAllPokemonPacket(pasturePCGUIConfiguration.pastureId).sendToServer()
}
private val rangeButton = object : Button(x + 6, y + 174, 27, 17, lang("ui.pasture.range"), {
storageWidget.pcGui.setPastureRangeEditing(!pasturePCGUIConfiguration.editingRange)
}, DEFAULT_NARRATION) {
override fun playDownSound(soundManager: SoundManager) {
storageWidget.pcGui.playSound(CobblemonSounds.PC_CLICK)
}
}
val pastureScrollList = PasturePokemonScrollList(
listX = x + 6,
listY = y + 31,
@@ -70,11 +82,29 @@ class PastureWidget(
pastureScrollList.render(context, mouseX, mouseY, delta)
recallButton.render(context, mouseX, mouseY, delta)
rangeButton.message = lang(if (pasturePCGUIConfiguration.editingRange) "ui.pasture.storage" else "ui.pasture.range")
val buttonX = rangeButton.x
val buttonY = rangeButton.y
val highlighted = pasturePCGUIConfiguration.editingRange || rangeButton.isMouseOver(mouseX.toDouble(), mouseY.toDouble())
blitk(matrixStack = context.pose(), texture = buttonResource, x = buttonX, y = buttonY,
width = 23, height = 17, textureWidth = 70, textureHeight = 34)
blitk(matrixStack = context.pose(), texture = buttonResource, x = buttonX + 23, y = buttonY,
width = 4, height = 17, uOffset = 66, textureWidth = 70, textureHeight = 34)
if (highlighted) context.fill(buttonX + 2, buttonY + 2, buttonX + 26, buttonY + 15, 0x88FFFFFF.toInt())
val gearSize = 11
val gearX = buttonX + (rangeButton.width - gearSize) / 2
val gearY = buttonY + (rangeButton.height - gearSize) / 2
if (highlighted) blitk(matrixStack = context.pose(), texture = gearResource,
x = gearX + 1, y = gearY, width = gearSize, height = gearSize,
red = 0.2F, green = 0.2F, blue = 0.2F)
blitk(matrixStack = context.pose(), texture = gearResource,
x = gearX, y = gearY, width = gearSize, height = gearSize)
}
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (rangeButton.mouseClicked(pMouseX, pMouseY, pButton)) return true
if (recallButton.isHovered(pMouseX, pMouseY)) recallButton.mouseClicked(pMouseX, pMouseY, pButton)
if (pastureScrollList.isHovered(pMouseX, pMouseY)) pastureScrollList.mouseClicked(pMouseX, pMouseY, pButton)
return super.mouseClicked(pMouseX, pMouseY, pButton)
}
}
}
@@ -16,7 +16,6 @@ import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
@@ -65,9 +64,6 @@ class FilterWidget(
renderCursor(context, input)
if (iconHovered(mouseX, mouseY)) {
context.renderTooltip(Minecraft.getInstance().font, lang("ui.pc.filter.tooltip"), mouseX, mouseY)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
@@ -81,4 +77,4 @@ class FilterWidget(
fun iconHovered(mouseX: Int, mouseY: Int): Boolean = mouseX >= (x - 9) && mouseY >= (y + 3)
&& mouseX <= ((x - 9) + (ICON_SIZE * SCALE)) && mouseY <= ((y + 3) + (ICON_SIZE * SCALE))
}
}
@@ -20,6 +20,7 @@ import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.ExitButton
import com.cobblemon.mod.common.client.gui.pasture.PasturePCGUIConfiguration
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.summary.Summary
import com.cobblemon.mod.common.client.gui.summary.Summary.Companion.iconCosmeticItemResource
@@ -140,7 +141,10 @@ class PCGUI(
// Add Exit Button
this.addRenderableWidget(
ExitButton(pX = x + 320, pY = y + 186) {
if (::wallpaperWidget.isInitialized && wallpaperWidget.visible) {
if (storageWidget.editingPastureRange) {
setPastureRangeEditing(false)
playSound(CobblemonSounds.PC_CLICK)
} else if (::wallpaperWidget.isInitialized && wallpaperWidget.visible) {
wallpaperWidget.visible = false
configuration.showParty = true
for (button in optionButtons) button.highlighted = false
@@ -158,7 +162,7 @@ class PCGUI(
pX = x + 220,
pY = y + 16,
forward = true
) { storageWidget.box += 1 }
) { if (!storageWidget.editingPastureRange) storageWidget.box += 1 }
)
// Add Backwards Button
@@ -167,7 +171,7 @@ class PCGUI(
pX = x + 117,
pY = y + 16,
forward = false
) { storageWidget.box -= 1 }
) { if (!storageWidget.editingPastureRange) storageWidget.box -= 1 }
)
// Add Filter Widget
@@ -197,6 +201,8 @@ class PCGUI(
storageWidget = storageWidget
)
this.addRenderableWidget(boxNameWidget)
filterWidget.active = !storageWidget.editingPastureRange
boxNameWidget.active = !storageWidget.editingPastureRange
// Initialise box options if not pasture
if (storageWidget.pastureWidget == null) {
@@ -691,6 +697,10 @@ class PCGUI(
super.render(context, mouseX, mouseY, delta)
if (!storageWidget.editingPastureRange && filterWidget.iconHovered(mouseX, mouseY)) {
context.renderTooltip(Minecraft.getInstance().font, lang("ui.pc.filter.tooltip"), mouseX, mouseY)
}
// Item Tooltip
if (pokemon != null) {
val displayedItem = if (showCosmeticItem) pokemon.cosmeticItem else pokemon.heldItemNoCopy()
@@ -711,17 +721,34 @@ class PCGUI(
}
}
fun setPastureRangeEditing(editing: Boolean) {
val pasture = configuration as? PasturePCGUIConfiguration ?: return
pasture.editingRange = editing
if (!editing) storageWidget.pastureRangeWidget?.closeInfo()
filterWidget.active = !editing
boxNameWidget.active = !editing
focused = null
storageWidget.resetSelected()
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val rangeWidget = storageWidget.pastureRangeWidget
if (storageWidget.editingPastureRange && rangeWidget?.isInfoOpen == true &&
children().none { it is ExitButton && it.isHovered(mouseX, mouseY) }) {
return rangeWidget.mouseClicked(mouseX, mouseY, button)
}
// Trigger child component function as they will need to check the entire screen click area
if (::boxNameWidget.isInitialized) boxNameWidget.mouseClicked(mouseX, mouseY, button)
if (::boxNameWidget.isInitialized && !storageWidget.editingPastureRange) boxNameWidget.mouseClicked(mouseX, mouseY, button)
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (storageWidget.editingPastureRange &&
storageWidget.pastureRangeWidget?.mouseScrolled(mouseX, mouseY, amount, verticalAmount) == true) return true
storageWidget.pastureWidget?.let { pasture ->
if (pasture.pastureScrollList.isHovered) pasture.pastureScrollList.mouseScrolled(mouseX, mouseY, amount, verticalAmount)
}
if (storageWidget.isHovered && mouseX < (storageWidget.x + StorageWidget.SCREEN_WIDTH)) this.storageWidget.box -= verticalAmount.toInt()
if (!storageWidget.editingPastureRange && storageWidget.isHovered && mouseX < (storageWidget.x + StorageWidget.SCREEN_WIDTH)) this.storageWidget.box -= verticalAmount.toInt()
if (::wallpaperWidget.isInitialized && wallpaperWidget.isHovered) wallpaperWidget.mouseScrolled(mouseX, mouseY, amount, verticalAmount)
val infoBoxX = ((width - BASE_WIDTH) / 2) + 9
@@ -744,6 +771,11 @@ class PCGUI(
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (storageWidget.editingPastureRange) {
if (keyCode != InputConstants.KEY_ESCAPE &&
storageWidget.pastureRangeWidget?.keyPressed(keyCode, scanCode, modifiers) == true) return true
if (keyCode == InputConstants.KEY_RIGHT || keyCode == InputConstants.KEY_LEFT) return true
}
val boxNameFocused = this::boxNameWidget.isInitialized && boxNameWidget.isFocused
val filterFocused = this::filterWidget.isInitialized && filterWidget.isFocused
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.pasture.PasturePCGUIConfiguration
import com.cobblemon.mod.common.client.gui.pasture.PastureWidget
import com.cobblemon.mod.common.client.gui.pasture.PastureRangeWidget
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.gui.PCBoxWallpaperRepository
@@ -74,6 +75,9 @@ class StorageWidget(
private val releaseNoButton: ReleaseConfirmButton
var pastureWidget: PastureWidget? = null
val pastureRangeWidget = (pcGui.configuration as? PasturePCGUIConfiguration)?.let { PastureRangeWidget(x, y, it) }
val editingPastureRange: Boolean
get() = (pcGui.configuration as? PasturePCGUIConfiguration)?.editingRange == true
var displayConfirmRelease = false
var screenLoaded = false
@@ -218,6 +222,11 @@ class StorageWidget(
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
if (editingPastureRange) {
pastureRangeWidget?.render(context, mouseX, mouseY, delta)
pastureWidget?.render(context, mouseX, mouseY, delta)
return
}
val matrices = context.pose()
// Party Label
if (pcGui.configuration.showParty) {
@@ -340,6 +349,10 @@ class StorageWidget(
}
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (editingPastureRange) {
return pastureRangeWidget?.mouseClicked(pMouseX, pMouseY, pButton) == true ||
pastureWidget?.mouseClicked(pMouseX, pMouseY, pButton) == true
}
if (displayConfirmRelease) {
if (releaseYesButton.isHovered(pMouseX, pMouseY)) releaseYesButton.mouseClicked(pMouseX, pMouseY, pButton)
if (releaseNoButton.isHovered(pMouseX, pMouseY)) releaseNoButton.mouseClicked(pMouseX, pMouseY, pButton)
@@ -23,6 +23,8 @@ object OpenPastureHandler : ClientNetworkPacketHandler<OpenPasturePacket> {
pastureId = packet.pastureId,
limit = packet.limit,
permissions = packet.permissions,
range = packet.range,
facing = packet.facing,
pasturedPokemon = SettableObservable(packet.tetheredPokemon)
)
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.pasture
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.pasture.PasturePCGUIConfiguration
import com.cobblemon.mod.common.client.gui.pc.PCGUI
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureRangePacket
import net.minecraft.client.Minecraft
import net.minecraft.client.resources.sounds.SimpleSoundInstance
object UpdatePastureRangeHandler : ClientNetworkPacketHandler<UpdatePastureRangePacket> {
override fun handle(packet: UpdatePastureRangePacket, client: Minecraft) {
val configuration = (client.screen as? PCGUI)?.configuration as? PasturePCGUIConfiguration ?: return
if (configuration.pastureId != packet.pastureId) return
configuration.range = packet.range
client.soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.PC_CLICK, 1F))
}
}
@@ -0,0 +1,24 @@
/*
* 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.pasture
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.pc.PCGUI
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureRecoveryPacket
import net.minecraft.client.Minecraft
object UpdatePastureRecoveryHandler : ClientNetworkPacketHandler<UpdatePastureRecoveryPacket> {
override fun handle(packet: UpdatePastureRecoveryPacket, client: Minecraft) {
val slots = (client.screen as? PCGUI)?.storage?.pastureWidget?.pastureScrollList?.children() ?: return
slots.firstOrNull { it.pokemon.pokemonId == packet.pokemonId }?.pokemon?.apply {
healingAfterFaint = packet.fainted
respawnAt = packet.respawnAt
}
}
}
@@ -166,7 +166,6 @@ import net.minecraft.world.item.Items
import net.minecraft.world.level.Level
import net.minecraft.world.level.LightLayer
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.HorizontalDirectionalBlock
import net.minecraft.world.level.block.SuspiciousEffectHolder
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.gameevent.GameEvent
@@ -304,6 +303,7 @@ open class PokemonEntity(
var drops: DropTable? = null
var tethering: PokemonPastureBlockEntity.Tethering? = null
private var nextPastureReturnAttempt = 0
// TODO review if we still want this
var queuedToDespawn = false
@@ -738,34 +738,29 @@ open class PokemonEntity(
// Check to see if the entity needs recalled or repositioned if pastured.
fun checkPastureTether() {
// Teleport a pastured mon back to the pasture block if they are out of range
if (this.tethering != null && !this.tethering!!.box.contains(this.x, this.y, this.z)) {
// Gather the ideal position (behind the pastureBlock)
val pastureBlockPos = this.tethering!!.pasturePos
val state = this.level().getBlockState(pastureBlockPos)
// Check to ensure we are working with a pastureblock here and it hasn't been broken
if (state.block == CobblemonBlocks.PASTURE) {
val direction = state.getValue(HorizontalDirectionalBlock.FACING)
val width = this.boundingBox.xsize
val tpPos = pastureBlockPos.offset(direction.normal.multiply(ceil(width).toInt() + 1)).center
val heightAllowance = 4.0 // Allow spawning up to x blocks above the pastureBlock y level
// Attempt tp and recall if it fails
if (!this.randomTeleport(tpPos.x, tpPos.y + heightAllowance, tpPos.z, false)) {
// If the teleport was not successful then recall the mon
this.tethering = null
this.pokemon.recall()
} else {
// Stop any now invalid navigation post teleport
this.navigation.stop()
}
} else {
// If the block is destroyed or something else went wrong.
this.tethering = null
this.pokemon.recall()
}
val currentTethering = tethering ?: return
val world = level() as? ServerLevel ?: return
if (ticksLived == 1 && world.hasChunkAt(currentTethering.pasturePos)) {
val pasture = world.getBlockEntity(currentTethering.pasturePos) as? PokemonPastureBlockEntity
pasture?.tetheredPokemon?.find { it.tetheringId == currentTethering.tetheringId }?.let { tethering = it }
}
if (tethering!!.canRoamTo(x, z) || !world.hasChunkAt(currentTethering.pasturePos)) return
navigation.stop()
(moveControl as? PokemonMoveControl)?.apply { stop(); stopBanking() }
brain.eraseMemory(MemoryModuleType.WALK_TARGET)
deltaMovement = Vec3.ZERO
if (ticksLived < nextPastureReturnAttempt) return
val pasture = world.getBlockEntity(currentTethering.pasturePos) as? PokemonPastureBlockEntity
val position = pasture?.findSpawnPosition(this, blockPosition())
if (position != null) {
teleportTo(position.x, position.y, position.z)
nextPastureReturnAttempt = 0
} else if (pasture == null) {
tethering = null
pokemon.recall()
return
} else {
nextPastureReturnAttempt = ticksLived + 20
}
}
@@ -1800,7 +1795,7 @@ open class PokemonEntity(
removalObservable.emit(reason)
this.brain.clearMemories()
if (reason.shouldDestroy() && pokemon.tetheringId != null) {
if (reason.shouldDestroy() && pokemon.tetheringId != null && tethering == null) {
pokemon.tetheringId = null
}
if (evolutionEntity != null) {
@@ -29,6 +29,7 @@ import net.minecraft.tags.FluidTags
import net.minecraft.util.Mth
import net.minecraft.world.entity.ai.attributes.Attributes
import net.minecraft.world.entity.ai.control.MoveControl
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.phys.AABB
@@ -106,7 +107,14 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
if (banking) {
pokemonEntity.yRot += bankRightDegreesPerTick
pokemonEntity.travel(Vec3(0.0, bankUpwardsBlocksPerTick.toDouble(), bankForwardBlocksPerTick.toDouble()).scale(mediumSpeed.toDouble()))
val forward = bankForwardBlocksPerTick * mediumSpeed
val tethering = pokemonEntity.tethering
if (tethering?.canRoamTo(
pokemonEntity.x - Mth.sin(pokemonEntity.yRot.toRadians()) * forward,
pokemonEntity.z + Mth.cos(pokemonEntity.yRot.toRadians()) * forward
) != false) {
pokemonEntity.travel(Vec3(0.0, bankUpwardsBlocksPerTick.toDouble(), bankForwardBlocksPerTick.toDouble()).scale(mediumSpeed.toDouble()))
}
if (bankDurationTicks > 0) {
bankDurationTicks--
}
@@ -280,6 +288,16 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
}
}
}
val tethering = pokemonEntity.tethering
val motion = mob.deltaMovement
if (tethering?.canRoamTo(mob.x, mob.z) == true &&
!tethering.canRoamTo(mob.x + motion.x, mob.z + motion.z)) {
mob.navigation.stop()
pokemonEntity.brain.eraseMemory(MemoryModuleType.WALK_TARGET)
stop()
stopBanking()
mob.deltaMovement = Vec3(0.0, motion.y, 0.0)
}
}
/*
@@ -369,4 +387,4 @@ class PokemonMoveControl(val pokemonEntity: PokemonEntity) : MoveControl(pokemon
strafeForwards = 0.0f
strafeRight = 0.0f
}
}
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.net.messages.client.pasture
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pasture.PasturePermissions
import com.cobblemon.mod.common.api.pasture.PastureRange
import com.cobblemon.mod.common.client.net.pasture.OpenPastureHandler
import com.cobblemon.mod.common.entity.pokemon.PokemonBehaviourFlag
import com.cobblemon.mod.common.net.IntSize
@@ -19,6 +20,7 @@ import net.minecraft.world.item.ItemStack
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.core.Direction
/**
* Opens a pasture GUI using the provided data.
@@ -28,7 +30,7 @@ import net.minecraft.resources.ResourceLocation
* @author Hiroku
* @since April 9th, 2023
*/
class OpenPasturePacket(val pcId: UUID, val pastureId: UUID, val limit: Int, val tetheredPokemon: List<PasturePokemonDataDTO>, val permissions: PasturePermissions) : NetworkPacket<OpenPasturePacket> {
class OpenPasturePacket(val pcId: UUID, val pastureId: UUID, val limit: Int, val tetheredPokemon: List<PasturePokemonDataDTO>, val permissions: PasturePermissions, val range: PastureRange, val facing: Direction) : NetworkPacket<OpenPasturePacket> {
class PasturePokemonDataDTO(
val pokemonId: UUID,
val playerId: UUID,
@@ -39,7 +41,9 @@ class OpenPasturePacket(val pcId: UUID, val pastureId: UUID, val limit: Int, val
val heldItem: ItemStack,
val level: Int,
val entityKnown: Boolean,
var behaviourFlags: Set<PokemonBehaviourFlag> = emptySet()
var behaviourFlags: Set<PokemonBehaviourFlag> = emptySet(),
var healingAfterFaint: Boolean = false,
var respawnAt: Long = 0L
) {
companion object {
fun decode(buffer: RegistryFriendlyByteBuf): PasturePokemonDataDTO {
@@ -53,6 +57,8 @@ class OpenPasturePacket(val pcId: UUID, val pastureId: UUID, val limit: Int, val
val level = buffer.readSizedInt(IntSize.U_SHORT)
val entityKnown = buffer.readBoolean()
val behaviourFlags = buffer.readList { it.readEnumConstant(PokemonBehaviourFlag::class.java) }.toSet()
val healingAfterFaint = buffer.readBoolean()
val respawnAt = buffer.readLong()
return PasturePokemonDataDTO(
pokemonId = pokemonId,
@@ -64,7 +70,9 @@ class OpenPasturePacket(val pcId: UUID, val pastureId: UUID, val limit: Int, val
heldItem = heldItem,
level = level,
entityKnown = entityKnown,
behaviourFlags = behaviourFlags
behaviourFlags = behaviourFlags,
healingAfterFaint = healingAfterFaint,
respawnAt = respawnAt
)
}
}
@@ -80,6 +88,8 @@ class OpenPasturePacket(val pcId: UUID, val pastureId: UUID, val limit: Int, val
buffer.writeSizedInt(IntSize.U_SHORT, level)
buffer.writeBoolean(entityKnown)
buffer.writeCollection(behaviourFlags) { _, flag -> buffer.writeEnumConstant(flag) }
buffer.writeBoolean(healingAfterFaint)
buffer.writeLong(respawnAt)
}
}
@@ -95,7 +105,7 @@ class OpenPasturePacket(val pcId: UUID, val pastureId: UUID, val limit: Int, val
dtos.add(PasturePokemonDataDTO.decode(buffer))
}
val permissions = PasturePermissions.decode(buffer)
return OpenPasturePacket(pcId, pastureId, limit, dtos, permissions)
return OpenPasturePacket(pcId, pastureId, limit, dtos, permissions, PastureRange.decode(buffer), buffer.readEnum(Direction::class.java))
}
}
@@ -110,5 +120,7 @@ class OpenPasturePacket(val pcId: UUID, val pastureId: UUID, val limit: Int, val
tethered.encode(buffer)
}
permissions.encode(buffer)
range.encode(buffer)
buffer.writeEnum(facing)
}
}
@@ -0,0 +1,31 @@
/*
* 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.pasture
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pasture.PastureRange
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import java.util.UUID
class UpdatePastureRangePacket(val pastureId: UUID, val range: PastureRange) : NetworkPacket<UpdatePastureRangePacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(pastureId)
range.encode(buffer)
}
companion object {
val ID = cobblemonResource("update_pasture_range")
fun decode(buffer: RegistryFriendlyByteBuf) = UpdatePastureRangePacket(
buffer.readUUID(), PastureRange.decode(buffer)
)
}
}
@@ -0,0 +1,29 @@
/*
* 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.pasture
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import java.util.UUID
import net.minecraft.network.RegistryFriendlyByteBuf
data class UpdatePastureRecoveryPacket(val pokemonId: UUID, val fainted: Boolean, val respawnAt: Long) : NetworkPacket<UpdatePastureRecoveryPacket> {
override val id = ID
companion object {
val ID = cobblemonResource("update_pasture_recovery")
fun decode(buffer: RegistryFriendlyByteBuf) = UpdatePastureRecoveryPacket(buffer.readUUID(), buffer.readBoolean(), buffer.readLong())
}
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(pokemonId)
buffer.writeBoolean(fainted)
buffer.writeLong(respawnAt)
}
}
@@ -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.net.messages.server.pasture
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.Direction
import net.minecraft.network.RegistryFriendlyByteBuf
import java.util.UUID
class AdjustPastureRangePacket(val pastureId: UUID, val direction: Direction, val increase: Boolean) : NetworkPacket<AdjustPastureRangePacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUUID(pastureId)
buffer.writeEnum(direction)
buffer.writeBoolean(increase)
}
companion object {
val ID = cobblemonResource("adjust_pasture_range")
fun decode(buffer: RegistryFriendlyByteBuf) = AdjustPastureRangePacket(
buffer.readUUID(), buffer.readEnum(Direction::class.java), buffer.readBoolean()
)
}
}
@@ -0,0 +1,39 @@
/*
* 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.pasture
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.pasture.PastureLinkManager
import com.cobblemon.mod.common.api.pasture.PastureRange
import com.cobblemon.mod.common.block.entity.PokemonPastureBlockEntity
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureRangePacket
import com.cobblemon.mod.common.net.messages.server.pasture.AdjustPastureRangePacket
import net.minecraft.core.Direction
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object AdjustPastureRangeHandler : ServerNetworkPacketHandler<AdjustPastureRangePacket> {
override fun handle(packet: AdjustPastureRangePacket, server: MinecraftServer, player: ServerPlayer) {
val link = PastureLinkManager.getLinkByPlayer(player) ?: return
if (link.linkId != packet.pastureId || packet.direction.axis == Direction.Axis.Y) return
val pasture = player.serverLevel().getBlockEntity(link.pos) as? PokemonPastureBlockEntity ?: return
val range = pasture.getRange().adjusted(packet.direction, if (packet.increase) 1 else -1)
if (!range.isValid(PastureRange.MAX_DISTANCE)) {
UpdatePastureRangePacket(link.linkId, pasture.getRange()).sendToPlayer(player)
return
}
pasture.setRange(range)
for (viewer in player.serverLevel().players()) {
val viewerLink = PastureLinkManager.getLinkByPlayer(viewer) ?: continue
if (viewerLink.pos == link.pos && viewerLink.dimension == link.dimension) {
UpdatePastureRangePacket(viewerLink.linkId, range).sendToPlayer(viewer)
}
}
}
}
@@ -9,24 +9,18 @@
package com.cobblemon.mod.common.net.serverhandling.pasture
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.entity.pokemon.PokemonBehaviourFlag
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.api.pasture.PastureLinkManager
import com.cobblemon.mod.common.block.entity.PokemonPastureBlockEntity
import com.cobblemon.mod.common.net.messages.server.pasture.SetPastureConflictPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import com.cobblemon.mod.common.net.messages.client.pasture.UpdatePastureConflictFlagPacket
object SetPastureConflictHandler : ServerNetworkPacketHandler<SetPastureConflictPacket> {
override fun handle(packet: SetPastureConflictPacket, server: MinecraftServer, player: ServerPlayer) {
val world = player.level()
val entity = world.getEntitiesOfClass(PokemonEntity::class.java, player.boundingBox.inflate(64.0))
.firstOrNull { it.pokemon.uuid == packet.pokemonId }
?: return
val link = PastureLinkManager.getLinkByPlayer(player) ?: return
val pasture = player.level().getBlockEntity(link.pos) as? PokemonPastureBlockEntity ?: return
val tethering = pasture.tetheredPokemon.firstOrNull { it.pokemonId == packet.pokemonId && it.playerId == player.uuid } ?: return
pasture.setConflict(tethering, packet.enabled)
entity.setBehaviourFlag(PokemonBehaviourFlag.PASTURE_CONFLICT, packet.enabled)
entity.remakeBrain()
// Send packet from the server to client GUI to update the toggle button
UpdatePastureConflictFlagPacket(packet.pokemonId, packet.enabled).sendToPlayer(player)
}
}
}
@@ -1307,6 +1307,18 @@
"cobblemon.ui.sort.discovered": "Sort by discovered status",
"cobblemon.ui.pasture": "Pasture",
"cobblemon.ui.pasture.recall_all": "Recall All",
"cobblemon.ui.pasture.range": "Range",
"cobblemon.ui.pasture.storage": "PC",
"cobblemon.ui.pasture.fainted": "Fainted",
"cobblemon.ui.pasture.returning": "Returning",
"cobblemon.ui.pasture.ready": "Waiting",
"cobblemon.ui.pasture.range.left_click": "Left click",
"cobblemon.ui.pasture.range.right_click": "Right click",
"cobblemon.ui.pasture.range.help_title": "Range controls",
"cobblemon.ui.pasture.range.increase": "Increase",
"cobblemon.ui.pasture.range.decrease": "Decrease",
"cobblemon.ui.pasture.range.scroll_up": "Scroll up",
"cobblemon.ui.pasture.range.scroll_down": "Scroll down",
"cobblemon.pasture.too_many_nearby": "There are too many Pokémon nearby.",
"cobblemon.ui.interact.give.item": "Change held item",
"cobblemon.ui.interact.give.cosmetic_item": "Change cosmetic item",
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

@@ -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.api.pasture
import com.cobblemon.mod.common.block.entity.PokemonPastureBlockEntity.Tethering
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.util.UUID
class PastureRangeTest {
@Test
fun `spec examples allow zero sides while requiring eight blocks on both axes`() {
listOf(
PastureRange(0, 8, 8, 0), PastureRange(4, 4, 4, 4), PastureRange(2, 8, 6, 0)
).forEach { assertTrue(it.isValid(64), it.toString()) }
listOf(
PastureRange(8, 7, 8, 0), PastureRange(64, 4, 64, 3)
).forEach { assertFalse(it.isValid(64), it.toString()) }
}
@Test
fun `every side obeys the configured maximum and rejects negative or overflowing input`() {
assertTrue(PastureRange(32, 32, 32, 32).isValid(32))
assertTrue(PastureRange(64, 64, 64, 64).isValid(PastureRange.MAX_DISTANCE))
assertFalse(PastureRange(65, 64, 64, 64).isValid(PastureRange.MAX_DISTANCE))
listOf(
PastureRange(33, 32, 32, 32), PastureRange(32, 33, 32, 32),
PastureRange(32, 32, 33, 32), PastureRange(32, 32, 32, 33),
PastureRange(-1, 8, 9, 0), PastureRange(Int.MAX_VALUE, 8, Int.MAX_VALUE, 0)
).forEach { assertFalse(it.isValid(32), it.toString()) }
}
@Test
fun `an adjustment changes only the requested cardinal side`() {
val range = PastureRange(4, 4, 4, 4)
Direction.Plane.HORIZONTAL.forEach { direction ->
val changed = range.adjusted(direction, 1)
assertEquals(5, changed[direction])
assertEquals(4, changed[direction.opposite])
assertTrue(changed.isValid(32))
assertFalse(range.adjusted(direction, -1).isValid(32))
}
}
@Test
fun `saved bounds round trip at negative world coordinates`() {
val origin = BlockPos(-100, 70, -200)
val range = PastureRange(2, 8, 6, 0)
assertEquals(range, PastureRange.fromBounds(origin, range.minPos(origin), range.maxPos(origin)))
}
@Test
fun `a zero length facing side still offers spawn blocks elsewhere in the minimum range`() {
val range = PastureRange(0, 8, 8, 0)
val candidates = range.nearbyBlocks(BlockPos.ZERO, BlockPos(0, 0, -4), 1.0).toList()
assertEquals(BlockPos.ZERO, candidates.first())
assertTrue(BlockPos(7, 0, 7) in candidates)
assertTrue(candidates.all { it.x in 0..7 && it.z in 0..7 })
}
@Test
fun `return search starts at the nearest block that fits the pokemon inside the edge`() {
val range = PastureRange(4, 4, 4, 4)
val candidates = range.nearbyBlocks(BlockPos.ZERO, BlockPos(99, 0, 1), 2.0).toList()
assertEquals(BlockPos(2, 0, 1), candidates.first())
assertTrue(candidates.all { it.x in -3..2 && it.z in -3..2 })
}
@Test
fun `a pokemon wider than the entire range has no safe center`() {
assertTrue(PastureRange(0, 8, 8, 0).nearbyBlocks(BlockPos.ZERO, BlockPos.ZERO, 9.0).none())
}
@Test
fun `pasture bounds constrain horizontal movement without a ceiling`() {
val tether = tether()
assertTrue(tether.canRoamTo(BlockPos(0, -10000, 0)))
assertTrue(tether.canRoamTo(BlockPos(0, 10000, 0)))
assertFalse(tether.canRoamTo(-4.01, 0.0))
assertFalse(tether.canRoamTo(4.0, 0.0))
tether.maxRoamPos = BlockPos(8, 4, 4)
assertTrue(tether.canRoamTo(7.5, 0.0))
}
@Test
fun `fainting starts one day of recovery without restarting it each check`() {
val tether = tether()
tether.beginRecovery(100L, fainted = true)
assertEquals(24100L, tether.respawnAt)
assertTrue(tether.healingAfterFaint)
tether.beginRecovery(140L, fainted = true)
assertEquals(24100L, tether.respawnAt)
}
@Test
fun `accidental disappearance has a shorter cooldown and fainting takes precedence`() {
val tether = tether()
tether.beginRecovery(100L, fainted = false)
assertEquals(300L, tether.respawnAt)
assertFalse(tether.healingAfterFaint)
tether.beginRecovery(140L, fainted = true)
assertEquals(24140L, tether.respawnAt)
tether.beginRecovery(180L, fainted = false)
assertEquals(24140L, tether.respawnAt)
assertTrue(tether.healingAfterFaint)
}
private fun tether() = Tethering(
BlockPos(-4, -4, -4), BlockPos(4, 4, 4), UUID.randomUUID(), "Owner",
UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), 1, BlockPos.ZERO
)
}