This commit is contained in:
MeAlam1
2026-10-14 22:04:59 +02:00
parent 0c5d228a11
commit 674ade41b1
8 changed files with 112 additions and 373 deletions
@@ -197,6 +197,7 @@ import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetMarkingsPa
import com.cobblemon.mod.common.net.messages.server.pokemon.update.SetNicknamePacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.evolution.AcceptEvolutionPacket
import com.cobblemon.mod.common.net.messages.server.pokeradar.PokeRadarScanPacket
import com.cobblemon.mod.common.net.messages.server.pokeradar.PokeRadarTeleportPacket
import com.cobblemon.mod.common.net.messages.server.riding.DismountPokemonPacket
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateDriverInputPacket
import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdatePassengerPositionPacket
@@ -256,6 +257,7 @@ import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetItemHiddenH
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetMarkingsHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.SetNicknameHandler
import com.cobblemon.mod.common.net.serverhandling.pokeradar.PokeRadarScanHandler
import com.cobblemon.mod.common.net.serverhandling.pokeradar.PokeRadarTeleportHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DismountPokemonPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.DriverInputPacketHandler
import com.cobblemon.mod.common.net.serverhandling.riding.ServerboundUpdatePassengerPositionHandler
@@ -656,8 +658,9 @@ object CobblemonNetwork {
// Poke Radar
list.add(PacketRegisterInfo(PokeRadarScanPacket.ID, PokeRadarScanPacket::decode, PokeRadarScanHandler))
// Debug / cheats
list.add(PacketRegisterInfo(PokeRadarTeleportPacket.ID, PokeRadarTeleportPacket::decode, PokeRadarTeleportHandler))
// Debug / cheats
list.add(PacketRegisterInfo(RequestOpenRidingStatsDebugGUIPacket.ID, RequestOpenRidingStatsDebugGUIPacket::decode, RequestOpenRidingStatsDebugGUIHandler))
@@ -2,10 +2,13 @@ package com.cobblemon.mod.common.client.gui.pokeradar
import com.cobblemon.mod.common.client.pokeradar.PokeRadarScanResults
import com.cobblemon.mod.common.client.pokeradar.PokeRadarType
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.server.pokeradar.PokeRadarScanPacket
import com.cobblemon.mod.common.net.messages.server.pokeradar.PokeRadarTeleportPacket
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.core.BlockPos
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
@@ -17,6 +20,7 @@ class RadarGUI(
private val rowsPerPage = 10
private val rowHeight = 18
private val listWidth = 300
private val tpButtonWidth = 28
private var page = initialPage
override fun init() {
@@ -43,11 +47,42 @@ class RadarGUI(
.bounds(width / 2 + 20, height - 28, 40, 20)
.build()
)
val entries = PokeRadarScanResults.entries
if (entries.isEmpty()) return
val totalPages = maxOf(1, (entries.size + rowsPerPage - 1) / rowsPerPage)
page = Mth.clamp(page, 0, totalPages - 1)
val listLeft = width / 2 - listWidth / 2
val listTop = 30
val start = page * rowsPerPage
val end = Mth.clamp(start + rowsPerPage, 0, entries.size)
for (i in start until end) {
val entry = entries[i]
val level = Minecraft.getInstance().level ?: return
val entity = entry.entityId?.let { level.getEntity(it) } as? PokemonEntity
val currentPos: BlockPos? = entity?.blockPosition()
val rowY = listTop + (i - start) * rowHeight
addRenderableWidget(
Button.builder(Component.literal("TP")) {
PokeRadarTeleportPacket(
currentPos ?: Minecraft.getInstance().player?.blockPosition()
).sendToServer()
Minecraft.getInstance().setScreen(null)
}
.bounds(listLeft + listWidth - tpButtonWidth - 2, rowY, tpButtonWidth, rowHeight - 2)
.build()
)
}
}
private fun changePage(delta: Int) {
val totalPages = maxOf(1, (PokeRadarScanResults.entries.size + rowsPerPage - 1) / rowsPerPage)
page = Mth.clamp(page + delta, 0, totalPages - 1)
rebuild()
}
override fun render(g: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
@@ -57,7 +92,7 @@ class RadarGUI(
val listLeft = width / 2 - listWidth / 2
val listTop = 30
g.drawCenteredString(font, "PokeRadar Scan (${entries.size} results)", width / 2, 12, 0xFFFFFF)
g.drawCenteredString(font, "PokeRadar Scan (${entries.size})", width / 2, 12, 0xFFFFFF)
if (entries.isEmpty()) {
g.drawCenteredString(font, "No results", width / 2, height / 2, 0xAAAAAA)
@@ -65,25 +100,27 @@ class RadarGUI(
}
val totalPages = maxOf(1, (entries.size + rowsPerPage - 1) / rowsPerPage)
page = Mth.clamp(page, 0, totalPages - 1)
val start = page * rowsPerPage
val end = Mth.clamp(start + rowsPerPage, 0, entries.size)
val textRight = listLeft + listWidth - tpButtonWidth - 6
for (i in start until end) {
val entry = entries[i]
val level = Minecraft.getInstance().level ?: return
val entity = entry.entityId?.let { level.getEntity(it) } as? PokemonEntity
val pos: BlockPos? = entity?.blockPosition()
val rowY = listTop + (i - start) * rowHeight
val hovered = mouseX in listLeft..(listLeft + listWidth) && mouseY in rowY..(rowY + rowHeight - 2)
val hovered = mouseX in listLeft..textRight && mouseY in rowY..(rowY + rowHeight - 2)
g.fill(
listLeft, rowY, listLeft + listWidth, rowY + rowHeight - 2,
if (hovered) 0x80FFFFFF.toInt() else 0x40000000
)
val name = entry.pokemon?.species ?: entry.detail.displayName ?: entry.detail.id
val levelText = entry.levelRange?.let { "L${it.first}-${it.last}" } ?: "L?"
val text = "$name $levelText ${"%.2f".format(entry.globalShare)}% " +
"${"%.1f".format(entry.distanceFromOrigin)}m [${entry.detail.bucket}]"
val name = entry.pokemon?.species ?: "?"
val levelText = entry.pokemon?.level?.let { "$it" } ?: "?"
val text = "$name $levelText (${pos?.x}, ${pos?.y}, ${pos?.z})"
g.drawString(font, text, listLeft + 4, rowY + 4, 0xFFFFFF)
}
@@ -96,28 +133,4 @@ class RadarGUI(
0xFFFFFF
)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (super.mouseClicked(mouseX, mouseY, button)) return true
if (button != 0) return false
val entries = PokeRadarScanResults.entries
if (entries.isEmpty()) return false
val listLeft = width / 2 - listWidth / 2
val listTop = 30
val start = page * rowsPerPage
val end = Mth.clamp(start + rowsPerPage, 0, entries.size)
for (i in start until end) {
val rowY = listTop + (i - start) * rowHeight
if (mouseX in listLeft.toDouble()..(listLeft + listWidth).toDouble() &&
mouseY in rowY.toDouble()..(rowY + rowHeight - 2).toDouble()
) {
Minecraft.getInstance().setScreen(RadarPokemonDetailGUI(type, entries[i], page))
return true
}
}
return false
}
}
@@ -1,70 +0,0 @@
package com.cobblemon.mod.common.client.gui.pokeradar
import com.cobblemon.mod.common.client.pokeradar.PokeRadarType
import com.cobblemon.mod.common.pokeradar.SpawnCheckEntryData
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.network.chat.Component
class RadarPokemonDetailGUI(
override val type: PokeRadarType,
private val entry: SpawnCheckEntryData,
private val returnPage: Int
) : PokeRadarGUI(type) {
override fun init() {
addRenderableWidget(
Button.builder(Component.literal("< Back")) {
Minecraft.getInstance().setScreen(RadarGUI(type, returnPage))
}.bounds(10, 10, 60, 20).build()
)
super.init()
}
override fun render(g: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
super.render(g, mouseX, mouseY, partialTick)
val name = entry.pokemon?.species ?: entry.detail.displayName ?: entry.detail.id
g.drawCenteredString(font, name, width / 2, 15, 0xFFFFFF)
val lines = buildList {
add("ID: ${entry.detail.id}")
add("Type: ${entry.detail.type}")
add("Bucket: ${entry.detail.bucket}")
add("Level Range: ${entry.levelRange?.let { "${it.first}-${it.last}" } ?: "?"}")
add("")
add("Block Pos: ${entry.spawnable.position}")
add(
"World Pos: ${"%.1f".format(entry.worldPosition.x)}, ${"%.1f".format(entry.worldPosition.y)}, ${
"%.1f".format(
entry.worldPosition.z
)
}"
)
add("Distance: ${"%.2f".format(entry.distanceFromOrigin)}m")
add("")
add("Block Light: ${entry.spawnable.light}")
add("Sky Light: ${entry.spawnable.skyLight}")
add("Can See Sky: ${entry.spawnable.canSeeSky}")
add("")
add("Weight: ${"%.3f".format(entry.weight)}")
add("Weight Share @ Pos: ${"%.2f".format(entry.weightShareAtPosition)}%")
add("Global Share: ${"%.2f".format(entry.globalShare)}%")
if (entry.detail.validBiomes.isNotEmpty()) {
add("")
add("Valid Biomes (${entry.detail.validBiomes.size}):")
entry.detail.validBiomes.take(6).forEach { add(" - $it") }
if (entry.detail.validBiomes.size > 6) {
add(" ... +${entry.detail.validBiomes.size - 6} more")
}
}
}
var y = 40
for (line in lines) {
g.drawString(font, line, 20, y, 0xFFFFFF)
y += 11
}
}
}
@@ -0,0 +1,18 @@
package com.cobblemon.mod.common.net.messages.server.pokeradar
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.BlockPos
import net.minecraft.network.RegistryFriendlyByteBuf
class PokeRadarTeleportPacket(val pos: BlockPos? = BlockPos.ZERO) : NetworkPacket<PokeRadarTeleportPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBlockPos(pos ?: BlockPos.ZERO)
}
companion object {
val ID = cobblemonResource("poke_radar_teleport")
fun decode(buffer: RegistryFriendlyByteBuf) = PokeRadarTeleportPacket(buffer.readBlockPos())
}
}
@@ -1,6 +1,5 @@
package com.cobblemon.mod.common.net.serverhandling.pokeradar
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.client.pokeradar.PokeRadarScanResultPacket
import com.cobblemon.mod.common.net.messages.server.pokeradar.PokeRadarScanPacket
@@ -13,12 +12,7 @@ object PokeRadarScanHandler : ServerNetworkPacketHandler<PokeRadarScanPacket> {
override fun handle(packet: PokeRadarScanPacket, server: MinecraftServer, player: ServerPlayer) {
val radius = 16
val entries: List<SpawnCheckEntry> = SpawnExtensions.scanSpawnsInRadius(
player = player,
bucket = Cobblemon.bestSpawner.config.worldBuckets,
radius = radius,
cube = false
)
val entries = SpawnExtensions.scanPokemonInRadius(player = player, radius = radius, cube = false)
PokeRadarScanResultPacket(SpawnCheckEntry.toData(entries)).sendToPlayer(player)
}
@@ -0,0 +1,17 @@
package com.cobblemon.mod.common.net.serverhandling.pokeradar
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.server.pokeradar.PokeRadarTeleportPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
// TODO: Add Operator Check
object PokeRadarTeleportHandler : ServerNetworkPacketHandler<PokeRadarTeleportPacket> {
override fun handle(packet: PokeRadarTeleportPacket, server: MinecraftServer, player: ServerPlayer) {
player.teleportTo(
packet.pos?.x?.plus(0.5) ?: 0.0,
packet.pos?.y?.toDouble() ?: 0.0,
packet.pos?.z?.plus(0.5) ?: 0.0
)
}
}
@@ -2,158 +2,31 @@ package com.cobblemon.mod.common.pokeradar
import com.cobblemon.mod.common.api.net.Encodable
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import net.minecraft.core.BlockPos
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.phys.Vec3
// TODO: Temporary until the Data that the client needs is finalized
data class SpawnablePositionData(
val position: BlockPos,
val light: Int,
val skyLight: Int,
val canSeeSky: Boolean
) {
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBlockPos(position)
buffer.writeInt(light)
buffer.writeInt(skyLight)
buffer.writeBoolean(canSeeSky)
}
companion object {
fun decode(buffer: RegistryFriendlyByteBuf) = SpawnablePositionData(
position = buffer.readBlockPos(),
light = buffer.readInt(),
skyLight = buffer.readInt(),
canSeeSky = buffer.readBoolean()
)
fun of(spawnable: SpawnablePosition) = SpawnablePositionData(
position = spawnable.position,
light = spawnable.light,
skyLight = spawnable.skyLight,
canSeeSky = spawnable.canSeeSky
)
}
}
data class SpawnDetailData(
val type: String,
val id: String,
val displayName: String?,
val bucket: String,
val validBiomes: Set<ResourceLocation>
) {
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUtf(type)
buffer.writeUtf(id)
buffer.writeNullable(displayName) { buf, s -> buf.writeUtf(s) }
buffer.writeUtf(bucket)
buffer.writeVarInt(validBiomes.size)
validBiomes.forEach { buffer.writeResourceLocation(it) }
}
companion object {
fun decode(buffer: RegistryFriendlyByteBuf): SpawnDetailData {
val type = buffer.readUtf()
val id = buffer.readUtf()
val displayName = buffer.readNullable { it.readUtf() }
val bucket = buffer.readUtf()
val size = buffer.readVarInt()
val validBiomes = mutableSetOf<ResourceLocation>()
repeat(size) { validBiomes.add(buffer.readResourceLocation()) }
return SpawnDetailData(type, id, displayName, bucket, validBiomes)
}
fun of(detail: SpawnDetail) = SpawnDetailData(
type = detail.type,
id = detail.id,
displayName = detail.displayName,
bucket = detail.bucket,
validBiomes = detail.validBiomes
)
}
}
data class SpawnCheckEntryData(
val spawnable: SpawnablePositionData,
val detail: SpawnDetailData,
val pokemon: PokemonProperties?,
val levelRange: IntRange?,
val worldPosition: Vec3,
val bucket: Map<String, Float>,
val weight: Float,
val weightShareAtPosition: Float,
val globalShare: Float,
val distanceFromOrigin: Double
val entityId: Int? = null
) : Encodable {
override fun encode(buffer: RegistryFriendlyByteBuf) {
spawnable.encode(buffer)
detail.encode(buffer)
buffer.writeNullable(pokemon) { buf, p -> buf.writeUtf(p.asString()) }
buffer.writeNullable(levelRange) { buf, r ->
buf.writeInt(r.first)
buf.writeInt(r.last)
}
buffer.writeDouble(worldPosition.x)
buffer.writeDouble(worldPosition.y)
buffer.writeDouble(worldPosition.z)
buffer.writeVarInt(bucket.size)
bucket.forEach { (k, v) ->
buffer.writeUtf(k)
buffer.writeFloat(v)
}
buffer.writeFloat(weight)
buffer.writeFloat(weightShareAtPosition)
buffer.writeFloat(globalShare)
buffer.writeDouble(distanceFromOrigin)
buffer.writeNullable(entityId) { buf, id -> buf.writeVarInt(id) }
}
companion object {
fun decode(buffer: RegistryFriendlyByteBuf): SpawnCheckEntryData {
val spawnable = SpawnablePositionData.decode(buffer)
val detail = SpawnDetailData.decode(buffer)
val pokemon = buffer.readNullable { PokemonProperties.parse(it.readUtf()) }
val levelRange = buffer.readNullable { IntRange(it.readInt(), it.readInt()) }
val worldPosition = Vec3(buffer.readDouble(), buffer.readDouble(), buffer.readDouble())
val bucketSize = buffer.readVarInt()
val bucket = LinkedHashMap<String, Float>(bucketSize)
repeat(bucketSize) { bucket[buffer.readUtf()] = buffer.readFloat() }
val weight = buffer.readFloat()
val weightShareAtPosition = buffer.readFloat()
val globalShare = buffer.readFloat()
val distanceFromOrigin = buffer.readDouble()
return SpawnCheckEntryData(
spawnable = spawnable,
detail = detail,
pokemon = pokemon,
levelRange = levelRange,
worldPosition = worldPosition,
bucket = bucket,
weight = weight,
weightShareAtPosition = weightShareAtPosition,
globalShare = globalShare,
distanceFromOrigin = distanceFromOrigin
)
val entityId = buffer.readNullable { it.readVarInt() }
return SpawnCheckEntryData(pokemon, entityId)
}
}
}
data class SpawnCheckEntry(
val position: SpawnablePosition,
val detail: SpawnDetail,
val pokemon: PokemonProperties?,
val levelRange: IntRange?,
val worldPosition: Vec3,
val bucket: MutableMap<String, Float>,
val weight: Float,
val weightShareAtPosition: Float,
val globalShare: Float,
val distanceFromOrigin: Double,
val entityId: Int? = null
) {
companion object {
fun toData(entries: List<SpawnCheckEntry>): List<SpawnCheckEntryData> =
@@ -161,15 +34,7 @@ data class SpawnCheckEntry(
}
fun toData() = SpawnCheckEntryData(
spawnable = SpawnablePositionData.of(position),
detail = SpawnDetailData.of(detail),
pokemon = pokemon,
levelRange = levelRange,
worldPosition = worldPosition,
bucket = bucket,
weight = weight,
weightShareAtPosition = weightShareAtPosition,
globalShare = globalShare,
distanceFromOrigin = distanceFromOrigin
entityId = entityId
)
}
@@ -1,137 +1,36 @@
package com.cobblemon.mod.common.pokeradar
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.config
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePositionCalculator.Companion.prioritizedAreaCalculators
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.duck.ServerLevelDuck
import com.cobblemon.mod.common.util.spawner
import net.minecraft.network.chat.Component
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
import kotlin.math.max
object SpawnExtensions {
fun scanSpawnsInRadius(
private const val BLOCKS_PER_CHUNK = 16
fun scanPokemonInRadius(
player: ServerPlayer,
bucket: MutableMap<String, Float>,
radius: Int,
cube: Boolean = false,
maxDistance: Double = radius.toDouble()
maxDistance: Double = (radius * BLOCKS_PER_CHUNK).toDouble()
): List<SpawnCheckEntry> {
if (!config.enableSpawning) return emptyList()
val levelKey = (player.serverLevel() as ServerLevelDuck).`cobblemon$getLevelResourceLocation`()
if (levelKey in config.worldSpawningBlocklist) return emptyList()
val spawner: Spawner = player.spawner
val world: ServerLevel = player.serverLevel()
val cause = SpawnCause(spawner, player)
val diameter = radius * 2 + 1
val height = if (cube) diameter else config.spawningZoneHeight
val slice = Cobblemon.spawningZoneGenerator.generate(
spawner = spawner,
input = SpawningZoneInput(
cause = cause,
world = world,
baseX = Mth.ceil(player.x - diameter / 2.0),
baseY = Mth.ceil(player.y - height / 2.0),
baseZ = Mth.ceil(player.z - diameter / 2.0),
length = diameter,
height = height,
width = diameter
)
)
val positions: List<AreaSpawnablePosition> = Cobblemon.areaSpawnablePositionResolver
.resolve(spawner, prioritizedAreaCalculators, slice)
val radiusInBlocks = (radius * BLOCKS_PER_CHUNK).toDouble()
val verticalRadius = if (cube) radiusInBlocks else config.spawningZoneHeight / 2.0
val box = player.boundingBox.inflate(radiusInBlocks, verticalRadius, radiusInBlocks)
val origin = player.position()
val entries = ArrayList<SpawnCheckEntry>(positions.size)
val globalWeightByDetail = HashMap<SpawnDetail, Float>()
for (pos in positions) {
val wp = Vec3(pos.position.x + 0.5, pos.position.y.toDouble(), pos.position.z + 0.5)
if (maxDistance > 0 && wp.distanceTo(origin) > maxDistance) continue
val matching = LinkedHashMap<SpawnDetail, Float>()
for (bucketName in bucket.keys) {
for (detail in spawner.getMatchingSpawns(bucketName, pos)) {
if (!pos.preFilter(detail) || !pos.postFilter(detail)) continue
val w = max(0F, pos.getWeight(detail))
if (w <= 0F) continue
matching.merge(detail, w, Float::plus)
}
}
if (matching.isEmpty()) continue
val positionTotal = matching.values.sum().takeIf { it > 0F } ?: 1F
for ((detail, weight) in matching) {
globalWeightByDetail.merge(detail, weight, Float::plus)
entries.add(
SpawnCheckEntry(
position = pos,
detail = detail,
pokemon = (detail as? PokemonSpawnDetail)?.pokemon,
levelRange = (detail as? PokemonSpawnDetail)?.levelRange,
worldPosition = wp,
bucket = bucket,
weight = weight,
weightShareAtPosition = weight / positionTotal * 100F,
globalShare = 0F,
distanceFromOrigin = origin.distanceTo(wp),
)
)
}
}
val globalTotal = globalWeightByDetail.values.sum().takeIf { it > 0F } ?: 1F
return entries.map { entry ->
val g = (globalWeightByDetail[entry.detail] ?: 0F) / globalTotal * 100F
entry.copy(globalShare = g)
}
}
}
// TODO: Debug
fun ServerPlayer.sendSpawnScan(entries: List<SpawnCheckEntry>, bucket: MutableMap<String, Float>) {
if (entries.isEmpty()) {
sendSystemMessage(Component.literal("No spawns for $bucket."))
return
}
val grouped = entries
.groupBy { it.pokemon?.species ?: it.detail.displayName ?: "?" }
sendSystemMessage(
Component.literal(
"${entries.size} entries across ${grouped.size} species in bucket $bucket:"
)
)
grouped.entries
.sortedByDescending { (_, list) -> list.maxOf { it.globalShare } }
.forEach { (name, list) ->
val nearest = list.minBy { it.distanceFromOrigin }
sendSystemMessage(
Component.literal(
" $name — ${"%.2f".format(nearest.globalShare)}% " +
"(${list.size} pos, nearest @ ${nearest.position.position} " +
"${"%.1f".format(nearest.distanceFromOrigin)}m, " +
"light=${nearest.position.light}/sky=${nearest.position.skyLight}, " +
"biome=${nearest.position.biomeName})"
)
return world.getEntitiesOfClass(PokemonEntity::class.java, box) { entity ->
entity.isAlive &&
entity.pokemon.isWild() &&
(maxDistance <= 0 || entity.position().distanceTo(origin) <= maxDistance)
}.map { entity ->
SpawnCheckEntry(
pokemon = PokemonProperties.parse(entity.pokemon.species.resourceIdentifier.toString()),
entityId = entity.id
)
}
}
}