Phase 2 of cursed merge, Habitats now only extends 1.8.0 branch

This commit is contained in:
Paul
2026-04-25 12:36:04 +10:00
parent 5c18bfdad3
commit 8c7478d00a
142 changed files with 5440 additions and 1159 deletions
+1
View File
@@ -11,6 +11,7 @@
- Added Deepslate Core Block
- Added /givetm Command
- Added Move Dex to Pokedex
- Added the Habitat Block, a spawner block for servers and adventure maps that controls spawning in an area.
- Added Disk Rack for storing disks and being an audio sequencer for Noteblocks
- Added intrinsic scaling to all mons (+-5%). Some rendering cases such as shoulder mounting revert to 1.0 scaling to preserve animation accuracy.
- Added scaling to "baby" pokemon (smaller (-20%) at level 1 -> normal sized at level 10)
@@ -18,6 +18,7 @@ import com.cobblemon.mod.common.api.drop.EvolutionItemDropEntry
import com.cobblemon.mod.common.api.drop.ItemDropEntry
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.CobblemonEvents.DATA_SYNCHRONIZED
import com.cobblemon.mod.common.api.habitats.HabitatPools
import com.cobblemon.mod.common.api.interaction.RequestManager
import com.cobblemon.mod.common.api.molang.MoLangLoadedFilesCache
import com.cobblemon.mod.common.api.molang.ObjectValue
@@ -92,11 +93,11 @@ import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacke
import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.aspects.MARK_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.ALPHA_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.CHARACTERISTIC_RAINBOW_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.COSMETIC_SLOT_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.GENDER_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.MARK_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.pokemon.evolution.variants.BlockClickEvolution
import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeature
@@ -366,6 +367,7 @@ object Cobblemon {
val server = event.server
MoLangLoadedFilesCache.initialize(server)
playerDataManager = PlayerInstancedDataStoreManager().also { it.setup(server) }
HabitatPools.onServerLoading(server)
val mongoClient: MongoClient?
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.block.entity.*
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.block.multiblock.FossilMultiblockBuilder
import com.cobblemon.mod.common.platform.PlatformRegistry
import net.minecraft.core.Registry
@@ -47,8 +48,8 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
val TM_MACHINE: BlockEntityType<TMMachineBlockEntity> = this.create("tm_machine", BlockEntityType.Builder.of(::TMMachineBlockEntity, CobblemonBlocks.TM_MACHINE).build(null))
@JvmField
val GROTTO_BLOCK: BlockEntityType<GrottoBlockEntity> = this.create("grotto_block",
BlockEntityType.Builder.of(::GrottoBlockEntity, CobblemonBlocks.GROTTO_BLOCK).build(null)
val HABITAT_BLOCK: BlockEntityType<HabitatBlockEntity> = this.create("habitat_block",
BlockEntityType.Builder.of(::HabitatBlockEntity, CobblemonBlocks.HABITAT_BLOCK).build(null)
)
@JvmField
@@ -20,6 +20,7 @@ import com.cobblemon.mod.common.block.general.BaleBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationCarpetBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationalBlock
import com.cobblemon.mod.common.block.grower.SaccharineTreeGrower
import com.cobblemon.mod.common.block.habitat.HabitatBlock
import com.cobblemon.mod.common.block.sign.CobblemonHangingSignBlock
import com.cobblemon.mod.common.block.sign.CobblemonSignBlock
import com.cobblemon.mod.common.block.sign.CobblemonWallHangingSignBlock
@@ -537,11 +538,16 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
)
@JvmField
val GROTTO_BLOCK = this.create("grotto_block", GrottoBlock(BlockBehaviour.Properties.of()
.mapColor(MapColor.STONE)
.strength(1.5F)
.pushReaction(PushReaction.BLOCK)
))
val HABITAT_BLOCK = this.create(
name = "habitat_block",
entry = HabitatBlock(
properties = BlockBehaviour.Properties.of()
.mapColor(MapColor.STONE)
.strength(1.5F, Blocks.OBSIDIAN.explosionResistance)
.pushReaction(PushReaction.BLOCK)
)
)
@JvmField
val CAMPFIRE = create("campfire", CampfireBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.PODZOL).strength(2.0F).lightLevel{14}, false))
@@ -445,9 +445,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
//@JvmField
//val STICKY_GLOB = this.create("sticky_glob", StickyGlobItem(Item.Settings()))
@JvmField
val GROTTO_BLOCK = blockItem("grotto_block", CobblemonBlocks.GROTTO_BLOCK)
@JvmField
val RESTORATION_TANK = blockItem("restoration_tank", CobblemonBlocks.RESTORATION_TANK)
@JvmField
@@ -607,6 +604,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val BERRY_JUICE = this.create("berry_juice", BerryJuiceItem())
@JvmField
val HABITAT_BLOCK = blockItem("habitat_block", CobblemonBlocks.HABITAT_BLOCK)
@JvmField
val GALARICA_NUTS = compostableItemNameBlockItem("galarica_nuts", CobblemonBlocks.GALARICA_NUT_BUSH)
@@ -35,6 +35,7 @@ import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.SetActiveTMPacketHandler
import com.cobblemon.mod.common.client.net.gui.SetTMMachineContainerDataPacketHandler
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
import com.cobblemon.mod.common.client.net.habitat.OpenHabitatBlockEditorHandler
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
import com.cobblemon.mod.common.client.net.orientation.ClientboundUpdateOrientationHandler
@@ -89,6 +90,7 @@ import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacke
import com.cobblemon.mod.common.net.messages.client.effect.SaccharineLogBlockParticlesPacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormEntityParticlePacket
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormParticlePacket
import com.cobblemon.mod.common.net.messages.client.habitat.OpenHabitatBlockEditorPacket
import com.cobblemon.mod.common.net.messages.client.npc.CloseNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.npc.OpenNPCEditorPacket
import com.cobblemon.mod.common.net.messages.client.orientation.ClientboundUpdateDriverInputPacket
@@ -171,9 +173,9 @@ import com.cobblemon.mod.common.net.messages.server.debug.OpenRidingStatsDebugGU
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingSettingsPacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingStatRangePacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingStatsPacket
import com.cobblemon.mod.common.net.messages.server.debug.UpdateGrottoBlockPacket
import com.cobblemon.mod.common.net.messages.server.dialogue.EscapeDialoguePacket
import com.cobblemon.mod.common.net.messages.server.dialogue.InputToDialoguePacket
import com.cobblemon.mod.common.net.messages.server.habitat.SaveHabitatBlockSettingsPacket
import com.cobblemon.mod.common.net.messages.server.npc.SaveNPCPacket
import com.cobblemon.mod.common.net.messages.server.orientation.ServerboundUpdateOrientationPacket
import com.cobblemon.mod.common.net.messages.server.pasture.PasturePokemonPacket
@@ -229,10 +231,10 @@ import com.cobblemon.mod.common.net.serverhandling.debug.RequestOpenRidingStatsD
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingSettingsHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingStatRangeHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingStatsHandler
import com.cobblemon.mod.common.net.serverhandling.debug.UpdateGrottoBlockHandler
import com.cobblemon.mod.common.net.serverhandling.dialogue.EscapeDialogueHandler
import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandler
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
import com.cobblemon.mod.common.net.serverhandling.habitat.SaveHabitatBlockSettingsHandler
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
@@ -504,9 +506,13 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(ClientboundUpdateDriverInputPacket.ID, ClientboundUpdateDriverInputPacket::decode, ClientboundUpdateDriverInputHandler))
list.add(PacketRegisterInfo(ClientboundSeatAssignmentPacket.ID, ClientboundSeatAssignmentPacket::decode, ClientboundSeatAssignmentPacketHandler))
// Habitat blocks
list.add(PacketRegisterInfo(OpenHabitatBlockEditorPacket.ID, OpenHabitatBlockEditorPacket::decode, OpenHabitatBlockEditorHandler))
// Debug
list.add(PacketRegisterInfo(OpenRidingStatsDebugGUIPacket.ID, OpenRidingStatsDebugGUIPacket::decode, OpenRidingStatsDebugGUIHandler))
return list
}
@@ -632,10 +638,13 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(SetEntityBehaviourPacket.ID, SetEntityBehaviourPacket::decode, SetEntityBehaviourHandler))
list.add(PacketRegisterInfo(DamageOnCollisionPacket.ID, DamageOnCollisionPacket::decode, DamageOnCollisionPacketHandler))
// Habitat blocks
list.add(PacketRegisterInfo(SaveHabitatBlockSettingsPacket.ID, SaveHabitatBlockSettingsPacket::decode, SaveHabitatBlockSettingsHandler))
// Debug / cheats
list.add(PacketRegisterInfo(UpdateGrottoBlockPacket.ID, UpdateGrottoBlockPacket::decode, UpdateGrottoBlockHandler))
list.add(PacketRegisterInfo(RequestOpenRidingStatsDebugGUIPacket.ID, RequestOpenRidingStatsDebugGUIPacket::decode, RequestOpenRidingStatsDebugGUIHandler))
return list
}
@@ -30,6 +30,10 @@ object CobblemonPoiTypes: PlatformRegistry<Registry<PoiType>, ResourceKey<Regist
// TODO after 1.7
// @JvmField
// val INCENSE_SWEET_KEY = createKey("incense_sweet", CobblemonBlocks.INCENSE_SWEET, 0, 1)
@JvmField
val HABITAT_BLOCK_KEY: ResourceKey<PoiType> = createKey("habitat_block", CobblemonBlocks.HABITAT_BLOCK, 0, 1)
@JvmField
val HABITAT_BLOCK = create(HABITAT_BLOCK_KEY.location().path, PoiType(getBlockStates(CobblemonBlocks.HABITAT_BLOCK), 0, 1))
private fun createKey(string: String, block: Block, maxTickets: Int, validRange: Int ): ResourceKey<PoiType> =
ResourceKey.create(Registries.POINT_OF_INTEREST_TYPE, cobblemonResource(string)).also { resourceKey ->
@@ -23,9 +23,21 @@ import com.cobblemon.mod.common.api.events.berry.BerryMutationResultEvent
import com.cobblemon.mod.common.api.events.berry.BerryYieldCalculationEvent
import com.cobblemon.mod.common.api.events.cooking.PokeSnackSpawnPokemonEvent
import com.cobblemon.mod.common.api.events.drops.LootDroppedEvent
import com.cobblemon.mod.common.api.events.entity.*
import com.cobblemon.mod.common.api.events.entity.PokemonEntityLoadEvent
import com.cobblemon.mod.common.api.events.entity.PokemonEntitySaveEvent
import com.cobblemon.mod.common.api.events.entity.PokemonEntitySaveToWorldEvent
import com.cobblemon.mod.common.api.events.entity.SpawnBucketChosenEvent
import com.cobblemon.mod.common.api.events.entity.SpawnEvent
import com.cobblemon.mod.common.api.events.farming.ApricornHarvestEvent
import com.cobblemon.mod.common.api.events.fishing.*
import com.cobblemon.mod.common.api.events.fishing.BaitConsumedEvent
import com.cobblemon.mod.common.api.events.fishing.BaitEffectFunctionRegistryEvent
import com.cobblemon.mod.common.api.events.fishing.BaitSetEvent
import com.cobblemon.mod.common.api.events.fishing.BaitSpawnPokemonEvent
import com.cobblemon.mod.common.api.events.fishing.BobberSpawnPokemonEvent
import com.cobblemon.mod.common.api.events.fishing.PokerodCastEvent
import com.cobblemon.mod.common.api.events.fishing.PokerodReelEvent
import com.cobblemon.mod.common.api.events.habitats.HabitatSpawnActivatedEvent
import com.cobblemon.mod.common.api.events.habitats.SaveHabitatBlockSettingsEvent
import com.cobblemon.mod.common.api.events.item.LeftoversCreatedEvent
import com.cobblemon.mod.common.api.events.pokeball.PokeBallCaptureCalculatedEvent
import com.cobblemon.mod.common.api.events.pokeball.PokemonCatchRateEvent
@@ -40,7 +52,11 @@ import com.cobblemon.mod.common.api.events.pokemon.healing.PokemonHealedEvent
import com.cobblemon.mod.common.api.events.pokemon.interaction.ExperienceCandyUseEvent
import com.cobblemon.mod.common.api.events.pokemon.interaction.PokemonInteractionGUICreationEvent
import com.cobblemon.mod.common.api.events.starter.StarterChosenEvent
import com.cobblemon.mod.common.api.events.storage.*
import com.cobblemon.mod.common.api.events.storage.ChangePCBoxWallpaperEvent
import com.cobblemon.mod.common.api.events.storage.ReleasePokemonEvent
import com.cobblemon.mod.common.api.events.storage.RenamePCBoxEvent
import com.cobblemon.mod.common.api.events.storage.WallpaperCollectionEvent
import com.cobblemon.mod.common.api.events.storage.WallpaperUnlockedEvent
import com.cobblemon.mod.common.api.events.world.BigRootPropagatedEvent
import com.cobblemon.mod.common.api.reactive.CancelableObservable
import com.cobblemon.mod.common.api.reactive.EventObservable
@@ -365,4 +381,12 @@ object CobblemonEvents {
// -------------------------------------------------------------------------------------
@JvmField
val SELECT_DRIVER = EventObservable<SelectDriverEvent>()
// Habitats
@JvmField
val HABITAT_SPAWN_ACTIVATED = CancelableObservable<HabitatSpawnActivatedEvent>()
@JvmField
val HABITAT_SETTINGS_SAVED_PRE = CancelableObservable<SaveHabitatBlockSettingsEvent.Pre>()
@JvmField
val HABITAT_SETTINGS_SAVED_POST = EventObservable<SaveHabitatBlockSettingsEvent.Post>()
}
@@ -10,7 +10,9 @@ package com.cobblemon.mod.common.api.events.entity
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import net.minecraft.world.entity.Entity
/**
@@ -21,4 +23,9 @@ import net.minecraft.world.entity.Entity
* @author Hiroku
* @since April 22nd, 2023
*/
class SpawnEvent<T : Entity>(val entity: T, val spawnablePosition: SpawnablePosition) : Cancelable()
class SpawnEvent<T : Entity>(val entity: T, val spawnablePosition: SpawnablePosition) : Cancelable() {
val cause: SpawnCause
get() = spawnablePosition.cause
val spawner: Spawner
get() = spawnablePosition.spawner
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.habitats
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.api.habitats.spawningstyle.ActivatedHabitatSpawning
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
/**
* Event that fires when a habitat is about to attempt to spawn Pokémon. Cancelling this event will prevent
* the habitat block from attempting to spawn anything for this activation.
*
* @author Hiroku
* @since February 15th,2026
*/
class HabitatSpawnActivatedEvent(
/** The habitat block entity that is responsible for the spawn. */
val habitatBlockEntity: HabitatBlockEntity,
/** The activation settings. You could have got it from the block entity but this is easier. */
val activatedHabitatSpawning: ActivatedHabitatSpawning,
/** The [SpawnCause] that will be used in the spawn action. This may contain a player that is nearest to the block. */
var cause: SpawnCause,
/** The maximum number of Pokémon the block will consider spawning (provided there is space). */
var maxSpawns: Int
) : Cancelable() {
/** The spawner representing the habitat. */
val spawner: FixedAreaSpawner
get() = activatedHabitatSpawning.spawner
}
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.habitats
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.api.habitats.dto.HabitatSettingsDTO
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
interface SaveHabitatBlockSettingsEvent {
val player: ServerPlayer
val world: ServerLevel
val blockPos: BlockPos
val habitatBlockEntity: HabitatBlockEntity
val settings: HabitatSettingsDTO
class Pre(
override val player: ServerPlayer,
override val world: ServerLevel,
override val blockPos: BlockPos,
override val habitatBlockEntity: HabitatBlockEntity,
override val settings: HabitatSettingsDTO
) : SaveHabitatBlockSettingsEvent, Cancelable()
class Post(
override val player: ServerPlayer,
override val world: ServerLevel,
override val blockPos: BlockPos,
override val habitatBlockEntity: HabitatBlockEntity,
override val settings: HabitatSettingsDTO
) : SaveHabitatBlockSettingsEvent
}
@@ -0,0 +1,37 @@
/*
* 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.habitats
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* A [HabitatPool] of [ActivatedHabitatSpawn]s. Mainly just to help with deserialization and ensuring
* a single type for everything that's applied to a habitat block.
*
* @author Hiroku
* @since February 13th, 2026
*/
class ActivatedHabitatPool : HabitatPool<ActivatedHabitatSpawn>() {
override val type: ResourceLocation = ActivatedHabitatSpawn.TYPE
override var spawns: List<ActivatedHabitatSpawn> = listOf()
companion object {
fun default() = ActivatedHabitatPool().apply {
id = ResourceLocation("cobblemon", "custom__default_activated_pool")
name = "Custom"
}
}
override fun decode(buffer: RegistryFriendlyByteBuf, buckets: List<SpawnBucket>) {
super.decode(buffer, buckets)
spawns = buffer.readList { ActivatedHabitatSpawn().apply { decode(buffer, buckets) } }
}
}
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.habitats
import com.cobblemon.mod.common.util.cobblemonResource
/**
* A basic [HabitatSpawn] that will be triggered via activated spawning in a habitat. Nothing to it, really.
*
* @author Hiroku
* @since February 13th, 2026
*/
class ActivatedHabitatSpawn : HabitatSpawn() {
companion object {
val TYPE = cobblemonResource("activated")
}
}
@@ -0,0 +1,30 @@
/*
* 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.habitats
import com.cobblemon.mod.common.api.habitats.spawningstyle.ActivatedHabitatSpawning
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.world.entity.Entity
/**
* Influence added to the activated habitat spawner blocks so that they can track the entity and apply block modifiers.
*
* @author Hiroku
* @since February 15th, 2026
*/
class ActivatedHabitatSpawningInfluence(val activatedHabitatSpawning: ActivatedHabitatSpawning) : SpawningInfluence {
override fun affectSpawn(action: SpawnAction<*>, entity: Entity) {
if (entity is PokemonEntity) {
activatedHabitatSpawning.habitatBlockEntity.modifiers.apply(entity)
activatedHabitatSpawning.habitatBlockEntity.spawnedEntityIDs.add(entity.id)
}
}
}
@@ -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.api.habitats
/**
* Controls how habitats change through all the possible phases.
*
* @author Hiroku
* @since February 23rd, 2026
*/
enum class HabitatPhaseOrder {
/** Literally in ascending order, 1-2-3-4. */
SIMPLE,
/** It randomly chooses an order then uses that order predictably. 3-1-4-2-3-1-4-2 for example */
FIXED_RANDOM,
/** Completely random phase each day. Like, 1-1-2-4-1-2-1-2-4-1-3 */
FULL_RANDOM
}
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.habitats
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* A pool of [HabitatSpawn]s. This is either filled with [NaturalHabitatSpawn]s or [ActivatedHabitatSpawn]s depending
* on whether the pool is a [NaturalHabitatPool] or an [ActivatedHabitatPool]. The pool is responsible for converting
* its spawns into [PokemonSpawnDetail]s.
*
* @author Hiroku
* @since February 13th, 2026
*/
abstract class HabitatPool<T : HabitatSpawn> {
companion object {
val types = mutableMapOf(
ActivatedHabitatSpawn.TYPE to ActivatedHabitatPool::class.java,
NaturalHabitatSpawn.TYPE to NaturalHabitatPool::class.java
)
}
lateinit var id: ResourceLocation
var name: String = ""
abstract val type: ResourceLocation
abstract var spawns: List<T>
/** Whether a pool is editable is just down to whether it's not from a datapack. */
@Transient
var editable: Boolean = false
open fun validate() {
for (spawn in spawns) {
if (SpawnablePosition.getByName(spawn.spawnablePositionType) == null) {
throw IllegalArgumentException("$id mentions non-existent spawnable position type ${spawn.spawnablePositionType} on spawn for ${spawn.species}")
}
}
}
fun createSpawnDetails(habitatBlockEntity: HabitatBlockEntity): List<PokemonSpawnDetail> {
return spawns.mapNotNull { it.createSpawnDetail(habitatBlockEntity) }
}
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(id)
buffer.writeUtf(name)
buffer.writeCollection(spawns) { _, it -> it.encode(buffer) }
}
open fun decode(buffer: RegistryFriendlyByteBuf, buckets: List<SpawnBucket>) {
id = buffer.readResourceLocation()
name = buffer.readUtf()
}
}
@@ -0,0 +1,101 @@
/*
* 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.habitats
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.spawning.IntRanges
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.TimeRange
import com.cobblemon.mod.common.api.spawning.position.SpawnablePositionType
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.adapters.HabitatPoolAdapter
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
import com.cobblemon.mod.common.util.adapters.IntRangesAdapter
import com.cobblemon.mod.common.util.adapters.RegisteredSpawnablePositionAdapter
import com.cobblemon.mod.common.util.adapters.SpawnBucketAdapter
import com.cobblemon.mod.common.util.adapters.SpeciesAdapter
import com.cobblemon.mod.common.util.adapters.pokemonPropertiesShortAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.fromJson
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import java.io.File
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.world.level.storage.LevelResource
object HabitatPools : JsonDataRegistry<HabitatPool<*>> {
override val id = cobblemonResource("habitat_pools")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<HabitatPools>()
override val typeToken: TypeToken<HabitatPool<*>> = TypeToken.get(HabitatPool::class.java)
override val resourcePath = "habitat_pools"
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(HabitatPool::class.java, HabitatPoolAdapter)
.registerTypeAdapter(SpawnablePositionType::class.java, RegisteredSpawnablePositionAdapter)
.registerTypeAdapter(SpawnBucket::class.java, SpawnBucketAdapter)
.registerTypeAdapter(Species::class.java, SpeciesAdapter)
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapter(TimeRange::class.java, TimeRange.adapter)
.registerTypeAdapter(IntRanges::class.java, IntRangesAdapter.basic)
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
.setPrettyPrinting()
.create()
private val datapacked = mutableMapOf<ResourceLocation, HabitatPool<*>>()
private val worldSaved = mutableMapOf<ResourceLocation, HabitatPool<*>>()
val habitatPoolsById = mutableMapOf<ResourceLocation, HabitatPool<*>>()
private lateinit var worldSavedPoolsFolder: File
override fun sync(player: ServerPlayer) {}
fun onServerLoading(server: MinecraftServer) {
worldSavedPoolsFolder = server.getWorldPath(LevelResource.ROOT).resolve("habitat_pools").toFile()
if (!worldSavedPoolsFolder.exists()) {
return
}
worldSavedPoolsFolder.listFiles { it.endsWith(".json") }.forEach {
val identifier = ResourceLocation.parse(it.nameWithoutExtension)
val habitat = gson.fromJson<HabitatPool<*>>(it.reader())
habitat.id = identifier
habitat.validate()
habitat.editable = true
worldSaved[identifier] = habitat
}
setupHabitatPoolsById()
}
override fun reload(data: Map<ResourceLocation, HabitatPool<*>>) {
datapacked.clear()
datapacked.putAll(data)
data.entries.forEach { (identifier, habitat) ->
habitat.id = identifier
habitat.validate()
habitat.editable = false
}
setupHabitatPoolsById()
}
fun setupHabitatPoolsById() {
habitatPoolsById.clear()
habitatPoolsById.putAll(datapacked)
habitatPoolsById.putAll(worldSaved)
}
}
@@ -0,0 +1,167 @@
/*
* 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.habitats
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.IntRanges
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.TimeRange
import com.cobblemon.mod.common.api.spawning.condition.AppendageCondition
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
import com.cobblemon.mod.common.util.adapters.IntRangesAdapter
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.math.intersection
import com.cobblemon.mod.common.util.math.intersects
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* A kind of simplified spawn definition for the purposes of spawning from [HabitatBlockEntity] instances. This is a
* very shallow class tree, with [ActivatedHabitatSpawn] being a trivial subclass but the other subclass being
* [NaturalHabitatSpawn] which introduces the need to supply a spawn bucket.
*
* These have only a small subset of the properties that a regular [PokemonSpawnDetail] would have to align with the
* fact that these are configured from a simple GUI. There are limits to how many properties and conditions could be
* exposed in a GUI before it becomes too difficult for the average user to understand what's going on.
*
* [HabitatSpawn]s are loaded from datapacks as pools of spawns but can also be modified from in-game, though those
* made from in-game are stored separately to datapacks. Only non-datapacked habitat spawns can be modified.
*
* Upon edits to a habitat block, the spawn details are regenerated based off of these definitions.
*
* @author Hiroku
* @since February 13th, 2026
*/
sealed class HabitatSpawn {
/** The species that will be spawned. */
lateinit var species: Species
/** The kind of position the Pokémon will spawn in. Things like "grounded", "seafloor", "fishing". */
lateinit var spawnablePositionType: String
/** Selection weight. */
var weight: Float = 1F
/** The possible levels for this spawn. */
var levelRange: IntRange = 1..Int.MAX_VALUE // I can't access the up-to-date config value here.
/** Additional modifiers to apply to the Pokémon, such as alolan=true, ability=intimidate, etc. */
var modifiers: PokemonProperties? = null
/** Which phases of the habitat this spawn will run in. See [HabitatBlockEntity] for more information. */
var phases: IntRanges? = null
/** The Minecraft time ranges for this spawn. */
var timeRange: TimeRange? = null
/** The minimum light level that the Pokémon can spawn in. */
var minLight: Int? = null
/** The maximum light level that the Pokémon can spawn in. */
var maxLight: Int? = null
open fun writeToNBT(nbt: CompoundTag) {
nbt.putString(DataKeys.HABITAT_POOL_SPAWN_SPECIES, species.resourceIdentifier.toString())
nbt.putString(DataKeys.HABITAT_POOL_SPAWN_POSITION_TYPE, spawnablePositionType)
nbt.putFloat(DataKeys.HABITAT_POOL_SPAWN_WEIGHT, weight)
nbt.putString(DataKeys.HABITAT_POOL_SPAWN_LEVEL_RANGE, IntRangeAdapter.serialize(levelRange))
modifiers?.let { nbt.putString(DataKeys.HABITAT_POOL_SPAWN_MODIFIERS, it.asString()) }
phases?.let { nbt.putString(DataKeys.HABITAT_POOL_SPAWN_PHASES, IntRangesAdapter.basic.serialize(it)) }
timeRange?.let { nbt.putString(DataKeys.HABITAT_POOL_SPAWN_TIMES, TimeRange.adapter.serialize(it)) }
minLight?.let { nbt.putInt(DataKeys.HABITAT_POOL_SPAWN_MIN_LIGHT, it) }
maxLight?.let { nbt.putInt(DataKeys.HABITAT_POOL_SPAWN_MAX_LIGHT, it) }
}
open fun readFromNBT(nbt: CompoundTag) {
species = PokemonSpecies.getByIdentifier(nbt.getString(DataKeys.HABITAT_POOL_SPAWN_SPECIES).asIdentifierDefaultingNamespace())!!
spawnablePositionType = nbt.getString(DataKeys.HABITAT_POOL_SPAWN_POSITION_TYPE)
weight = nbt.getFloat(DataKeys.HABITAT_POOL_SPAWN_WEIGHT)
levelRange = IntRangeAdapter.deserialize(nbt.getString(DataKeys.HABITAT_POOL_SPAWN_LEVEL_RANGE))
modifiers = if (nbt.contains(DataKeys.HABITAT_POOL_SPAWN_MODIFIERS)) PokemonProperties.parse(nbt.getString(DataKeys.HABITAT_POOL_SPAWN_MODIFIERS)) else null
phases = if (nbt.contains(DataKeys.HABITAT_POOL_SPAWN_PHASES)) IntRangesAdapter.basic.deserialize(nbt.getString(DataKeys.HABITAT_POOL_SPAWN_PHASES)) else null
timeRange = if (nbt.contains(DataKeys.HABITAT_POOL_SPAWN_TIMES)) TimeRange.adapter.deserialize(nbt.getString(DataKeys.HABITAT_POOL_SPAWN_TIMES)) else null
minLight = if (nbt.contains(DataKeys.HABITAT_POOL_SPAWN_MIN_LIGHT)) nbt.getInt(DataKeys.HABITAT_POOL_SPAWN_MIN_LIGHT) else null
maxLight = if (nbt.contains(DataKeys.HABITAT_POOL_SPAWN_MAX_LIGHT)) nbt.getInt(DataKeys.HABITAT_POOL_SPAWN_MAX_LIGHT) else null
}
open fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(species.resourceIdentifier)
buffer.writeUtf(spawnablePositionType)
buffer.writeFloat(weight)
buffer.writeUtf(IntRangeAdapter.serialize(levelRange))
buffer.writeNullable(modifiers) { _, value -> buffer.writeUtf(value.asString()) }
buffer.writeNullable(phases) { _, value -> buffer.writeUtf(IntRangesAdapter.basic.serialize(value)) }
buffer.writeNullable(timeRange) { _, value -> buffer.writeUtf(TimeRange.adapter.serialize(value)) }
buffer.writeNullable(minLight) { _, value -> buffer.writeInt(value) }
buffer.writeNullable(maxLight) { _, value -> buffer.writeInt(value) }
}
open fun decode(
buffer: RegistryFriendlyByteBuf,
buckets: List<SpawnBucket>
) {
species = PokemonSpecies.getByIdentifier(buffer.readResourceLocation())!!
spawnablePositionType = buffer.readUtf()
weight = buffer.readFloat()
levelRange = IntRangeAdapter.deserialize(buffer.readUtf())
modifiers = buffer.readNullable { _ -> PokemonProperties.parse(buffer.readUtf()) }
phases = buffer.readNullable { _ -> IntRangesAdapter.basic.deserialize(buffer.readUtf()) }
timeRange = buffer.readNullable { _ -> TimeRange.adapter.deserialize(buffer.readUtf()) }
minLight = buffer.readNullable { _ -> buffer.readInt() }
maxLight = buffer.readNullable { _ -> buffer.readInt() }
}
/** Creates the [PokemonSpawnDetail] that will be used in the spawner. This is done per habitat block. */
open fun createSpawnDetail(habitatBlockEntity: HabitatBlockEntity): PokemonSpawnDetail? {
val spawnDetail = PokemonSpawnDetail()
// If the habitat block specifies a level range, it restricts how and if this spawn exists
if (!levelRange.intersects(habitatBlockEntity.levelRange)) {
return null
}
val spawnablePositionType = SpawnablePosition.getByName(this.spawnablePositionType) ?: return null // How'd they get bad data into here
val levelRange = levelRange.intersection(habitatBlockEntity.levelRange)
val condition = createCondition(habitatBlockEntity)
val properties = modifiers?.copy() ?: PokemonProperties()
properties.species = species.resourceIdentifier.path
spawnDetail.apply {
this.id = "habitat_${species.resourceIdentifier}"
this.pokemon = properties
this.bucket = BestSpawner.config.buckets.first { it.weight > 0 }
this.weight = this@HabitatSpawn.weight
this.levelRange = levelRange
this.conditions = mutableListOf(condition)
this.spawnablePositionType = spawnablePositionType
}
return spawnDetail
}
fun createCondition(habitatBlockEntity: HabitatBlockEntity): SpawningCondition<*> {
val clazz = SpawningCondition.getByName(SpawnablePosition.getByName(this.spawnablePositionType)!!.defaultCondition)!!
val condition = clazz.getConstructor().newInstance()
condition.timeRange = timeRange
condition.minLight = minLight
condition.maxLight = maxLight
val phases = phases
if (phases != null) {
condition.appendages.add(PhaseAppendageCondition(habitatBlockEntity, phases))
}
return condition
}
class PhaseAppendageCondition(val habitatBlockEntity: HabitatBlockEntity, val phases: IntRanges) : AppendageCondition {
override fun fits(spawnablePosition: SpawnablePosition) = habitatBlockEntity.currentPhase in phases
}
}
@@ -0,0 +1,37 @@
/*
* 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.habitats
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* A [HabitatPool] of [NaturalHabitatSpawn]s. Mainly just to help with deserialization and ensuring
* a single type for everything that's applied to a habitat block.
*
* @author Hiroku
* @since February 13th, 2026
*/
class NaturalHabitatPool : HabitatPool<NaturalHabitatSpawn>() {
override val type: ResourceLocation = NaturalHabitatSpawn.TYPE
override var spawns: List<NaturalHabitatSpawn> = listOf()
companion object {
fun default() = NaturalHabitatPool().apply {
id = ResourceLocation("cobblemon", "custom__default_natural_pool")
name = "Custom"
}
}
override fun decode(buffer: RegistryFriendlyByteBuf, buckets: List<SpawnBucket>) {
super.decode(buffer, buckets)
spawns = buffer.readList { NaturalHabitatSpawn().apply { decode(buffer, buckets) } }
}
}
@@ -0,0 +1,63 @@
/*
* 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.habitats
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* A type of [HabitatSpawn] that spawns Pokémon as part of natural world spawning, and therefore
* needs to indicate the [SpawnBucket] it will be part of.
*
* @author Hiroku
* @since February 13th, 2026
*/
class NaturalHabitatSpawn : HabitatSpawn() {
companion object {
val TYPE = cobblemonResource("natural")
}
lateinit var bucket: SpawnBucket
override fun createSpawnDetail(habitatBlockEntity: HabitatBlockEntity): PokemonSpawnDetail? {
val spawnDetail = super.createSpawnDetail(habitatBlockEntity)
spawnDetail?.bucket = bucket
return spawnDetail
}
override fun writeToNBT(nbt: CompoundTag) {
super.writeToNBT(nbt)
nbt.putString(DataKeys.HABITAT_POOL_SPAWN_BUCKET, bucket.name)
}
override fun readFromNBT(nbt: CompoundTag) {
super.readFromNBT(nbt)
bucket = Cobblemon.bestSpawner.config.buckets.first { it.name == nbt.getString(DataKeys.HABITAT_POOL_SPAWN_BUCKET) }
}
override fun encode(buffer: RegistryFriendlyByteBuf) {
super.encode(buffer)
buffer.writeUtf(bucket.name)
}
override fun decode(
buffer: RegistryFriendlyByteBuf,
buckets: List<SpawnBucket>
) {
super.decode(buffer, buckets)
val bucketName = buffer.readUtf()
bucket = buckets.first { it.name == bucketName }
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.habitats.dto
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatPool
import com.cobblemon.mod.common.api.habitats.spawningstyle.ActivatedHabitatSpawning
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import net.minecraft.network.RegistryFriendlyByteBuf
class ActivatedHabitatSettingsDTO() {
var chance = 1F
var trigger: ActivatedHabitatSpawning.Trigger = ActivatedHabitatSpawning.Trigger.REDSTONE
var cancelledNaturalSpawningRange = -1
var spawnRange = 16
var maxSpawns = -1
var maxSpawnsPerActivation = 1
var habitatPool: HabitatPoolSettingsDTO = HabitatPoolSettingsDTO(ActivatedHabitatPool.default())
constructor(activatedHabitatSpawning: ActivatedHabitatSpawning): this() {
this.chance = activatedHabitatSpawning.chance
this.trigger = activatedHabitatSpawning.trigger
this.cancelledNaturalSpawningRange = activatedHabitatSpawning.cancelledNaturalSpawningRange
this.spawnRange = activatedHabitatSpawning.spawnRange
this.maxSpawns = activatedHabitatSpawning.maxSpawns
this.maxSpawnsPerActivation = activatedHabitatSpawning.maxSpawnsPerActivation
this.habitatPool = HabitatPoolSettingsDTO(activatedHabitatSpawning.pool)
}
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeFloat(chance)
buffer.writeEnum(trigger)
buffer.writeInt(cancelledNaturalSpawningRange)
buffer.writeInt(spawnRange)
buffer.writeInt(maxSpawns)
buffer.writeInt(maxSpawnsPerActivation)
habitatPool.encode(buffer)
}
fun decode(
buffer: RegistryFriendlyByteBuf,
buckets: List<SpawnBucket>
) {
this.chance = buffer.readFloat()
this.trigger = buffer.readEnum(ActivatedHabitatSpawning.Trigger::class.java)
this.cancelledNaturalSpawningRange = buffer.readInt()
this.spawnRange = buffer.readInt()
this.maxSpawns = buffer.readInt()
this.maxSpawnsPerActivation = buffer.readInt()
this.habitatPool = HabitatPoolSettingsDTO()
habitatPool.decode(buffer, buckets)
}
}
@@ -0,0 +1,101 @@
/*
* 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.habitats.dto
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatPool
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatSpawn
import com.cobblemon.mod.common.api.habitats.HabitatPool
import com.cobblemon.mod.common.api.habitats.HabitatPools
import com.cobblemon.mod.common.api.habitats.HabitatSpawn
import com.cobblemon.mod.common.api.habitats.NaturalHabitatPool
import com.cobblemon.mod.common.api.habitats.NaturalHabitatSpawn
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
class HabitatPoolSettingsDTO() {
lateinit var id: ResourceLocation
var isReference = false
var isActivated = false
// Below properties are only there when it's not a reference, otherwise the handler will have to look them up from the id
var name: String? = null
var spawns: MutableList<HabitatSpawn>? = null
constructor(habitatPool: HabitatPool<*>): this() {
this.id = habitatPool.id
this.isReference = !id.path.startsWith("custom__")
this.isActivated = habitatPool is ActivatedHabitatPool
if (!this.isReference) {
this.name = habitatPool.name
this.spawns = habitatPool.spawns.toMutableList()
}
}
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(id)
buffer.writeBoolean(isReference)
buffer.writeBoolean(isActivated)
if (!isReference) {
buffer.writeUtf(name!!)
buffer.writeCollection(spawns!!) { _, spawn ->
spawn.encode(buffer)
}
}
}
fun decode(
buffer: RegistryFriendlyByteBuf,
buckets: List<SpawnBucket>
) {
this.id = buffer.readResourceLocation()
this.isReference = buffer.readBoolean()
this.isActivated = buffer.readBoolean()
if (!isReference) {
this.name = buffer.readUtf()
this.spawns = buffer.readList {
val spawn = if (isActivated) {
ActivatedHabitatSpawn()
} else {
NaturalHabitatSpawn()
}
spawn.decode(buffer, buckets)
spawn
}.toMutableList()
}
}
fun toHabitatPool(): HabitatPool<*>? {
if (isReference) {
val pool = HabitatPools.habitatPoolsById[id]
if (pool == null) {
Cobblemon.LOGGER.error("Could not find habitat pool with id $id")
} else if (pool is ActivatedHabitatPool && !isActivated) {
Cobblemon.LOGGER.error("Pool with id $id is an activated habitat pool but was deserialized as a natural pool")
} else if (pool !is ActivatedHabitatPool && isActivated) {
Cobblemon.LOGGER.error("Pool with id $id is a natural habitat pool but was deserialized as an activated pool")
}
return pool
}
return if (isActivated) {
val pool = ActivatedHabitatPool()
pool.id = id
pool.name = name!!
pool.spawns = spawns!!.map { it as ActivatedHabitatSpawn }
pool
} else {
val pool = NaturalHabitatPool()
pool.id = id
pool.name = name!!
pool.spawns = spawns!!.map { it as NaturalHabitatSpawn }
pool
}
}
}
@@ -0,0 +1,83 @@
/*
* 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.habitats.dto
import com.cobblemon.mod.common.api.habitats.HabitatPhaseOrder
import com.cobblemon.mod.common.api.habitats.spawningstyle.ActivatedHabitatSpawning
import com.cobblemon.mod.common.api.habitats.spawningstyle.NaturalHabitatSpawning
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
class HabitatSettingsDTO() {
var numberOfPhases = 1
var phaseOrder = HabitatPhaseOrder.SIMPLE
lateinit var levelRange: IntRange
var modifiers: String = ""
lateinit var mimicId: ResourceLocation
var isActivatedSpawning = false
var activatedSettings: ActivatedHabitatSettingsDTO? = null
var naturalSettings: NaturalHabitatSettingsDTO? = null
constructor(habitatBlockEntity: HabitatBlockEntity) : this() {
this.numberOfPhases = habitatBlockEntity.numberOfPhases
this.phaseOrder = habitatBlockEntity.phaseOrder
this.levelRange = habitatBlockEntity.levelRange
this.modifiers = habitatBlockEntity.modifiers.asString()
this.mimicId = habitatBlockEntity.mimicId
val style = habitatBlockEntity.spawningStyle
if (style is ActivatedHabitatSpawning) {
this.isActivatedSpawning = true
this.activatedSettings = ActivatedHabitatSettingsDTO(style)
} else if (style is NaturalHabitatSpawning) {
this.naturalSettings = NaturalHabitatSettingsDTO(style)
}
}
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(numberOfPhases)
buffer.writeEnum(phaseOrder)
buffer.writeInt(levelRange.first)
buffer.writeInt(levelRange.last)
buffer.writeUtf(modifiers)
buffer.writeResourceLocation(mimicId)
buffer.writeBoolean(isActivatedSpawning)
if (isActivatedSpawning) {
activatedSettings?.encode(buffer)
} else {
naturalSettings?.encode(buffer)
}
}
fun decode(
buffer: RegistryFriendlyByteBuf,
buckets: List<SpawnBucket>
) {
this.numberOfPhases = buffer.readInt()
this.phaseOrder = buffer.readEnum(HabitatPhaseOrder::class.java)
val levelRangeStart = buffer.readInt()
val levelRangeEnd = buffer.readInt()
this.levelRange = levelRangeStart..levelRangeEnd
this.modifiers = buffer.readUtf()
this.mimicId = buffer.readResourceLocation()
if (buffer.readBoolean()) {
this.isActivatedSpawning = true
this.naturalSettings = null
val activatedSettings = ActivatedHabitatSettingsDTO()
activatedSettings.decode(buffer, buckets)
this.activatedSettings = activatedSettings
} else {
this.activatedSettings = null
val naturalSettings = NaturalHabitatSettingsDTO()
naturalSettings.decode(buffer, buckets)
this.naturalSettings = naturalSettings
}
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.habitats.dto
import com.cobblemon.mod.common.api.habitats.NaturalHabitatPool
import com.cobblemon.mod.common.api.habitats.spawningstyle.NaturalHabitatSpawning
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import net.minecraft.network.RegistryFriendlyByteBuf
class NaturalHabitatSettingsDTO() {
var replaceSpawns = false
var rangeOfInfluence = 16
var habitatPool: HabitatPoolSettingsDTO = HabitatPoolSettingsDTO(NaturalHabitatPool.default())
constructor(naturalHabitatSpawning: NaturalHabitatSpawning): this() {
this.replaceSpawns = naturalHabitatSpawning.replaceSpawns
this.rangeOfInfluence = naturalHabitatSpawning.rangeOfInfluence
this.habitatPool = HabitatPoolSettingsDTO(naturalHabitatSpawning.pool)
}
fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBoolean(replaceSpawns)
buffer.writeInt(rangeOfInfluence)
habitatPool.encode(buffer)
}
fun decode(
buffer: RegistryFriendlyByteBuf,
buckets: List<SpawnBucket>
) {
this.replaceSpawns = buffer.readBoolean()
this.rangeOfInfluence = buffer.readInt()
this.habitatPool = HabitatPoolSettingsDTO()
habitatPool.decode(buffer, buckets)
}
}
@@ -0,0 +1,163 @@
/*
* 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.habitats.spawningstyle
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.habitats.HabitatSpawnActivatedEvent
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatPool
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatSpawn
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatSpawningInfluence
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.spawner.FixedAreaSpawner
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.util.DataKeys
import net.minecraft.core.BlockPos
import net.minecraft.nbt.CompoundTag
import net.minecraft.server.level.ServerLevel
/**
* A habitat spawning style defined by being triggered by something. The activation trigger, [trigger], is either
* a redstone pulse, every tick, or every random tick.
*
* The spawn details are generated from the [pool] every time there is some change to the habitat block's settings,
* then those spawn details are used for spawning Pokémon when the habitat block is triggered.
*
* The [chance] property is applied after the trigger logic so that not every activation necessarily results in a spawn
* attempt.
*
* @author Hiroku
* @since February 14th, 2026
*/
class ActivatedHabitatSpawning(val habitatBlockEntity: HabitatBlockEntity) : HabitatSpawningStyle {
enum class Trigger {
/** Requires a redstone pulse. */
REDSTONE,
/** Tries every tick. */
TICK,
/** Tries every random tick. */
RANDOM_TICK
}
override val type = ActivatedHabitatSpawn.TYPE
/** The chance that an activation by the [trigger] will result in a spawn attempt. The spawn attempt may still fail. */
var chance = 1F
/** The thing that will activate the habitat block into attempting to spawn something. */
var trigger = Trigger.REDSTONE
/** What range, in blocks, around the habitat block in which natural spawning will not occur. */
var cancelledNaturalSpawningRange = -1
/** The range, in blocks, around the habitat block in which spawning will be attempted. */
var spawnRange = 16
/** The maximum number of Pokémon that can exist from this block's spawning before it pauses. -1 for infinite. */
var maxSpawns = -1
/** The maximum number of Pokémon that can spawn in a single activation. This will still be capped by [maxSpawns]. */
var maxSpawnsPerActivation = 1
/** The pool of spawns to choose from. */
var pool: ActivatedHabitatPool = ActivatedHabitatPool.default()
// This is cached here so that it doesn't have to be regenerated every time the block is triggered, updated when settings change.
var spawnDetails: List<SpawnDetail> = listOf()
lateinit var spawner: FixedAreaSpawner
override fun writeToNBT(nbt: CompoundTag) {
nbt.putString(DataKeys.HABITAT_SPAWNING_STYLE, type.toString())
nbt.putFloat(DataKeys.HABITAT_ACTIVATED_CHANCE, chance)
nbt.putString(DataKeys.HABITAT_ACTIVATED_TRIGGER, trigger.name)
nbt.putInt(DataKeys.HABITAT_ACTIVATED_CANCEL_RANGE, cancelledNaturalSpawningRange)
nbt.putInt(DataKeys.HABITAT_ACTIVATED_SPAWN_RANGE, spawnRange)
nbt.putInt(DataKeys.HABITAT_ACTIVATED_MAX_SPAWNS, maxSpawns)
nbt.putInt(DataKeys.HABITAT_ACTIVATED_MAX_SPAWNS_PER_ACTIVATION, maxSpawnsPerActivation)
val poolNBT = CompoundTag()
writePoolToNBT(pool, poolNBT)
nbt.put(DataKeys.HABITAT_POOL, poolNBT)
}
override fun readFromNBT(nbt: CompoundTag) {
chance = nbt.getFloat(DataKeys.HABITAT_ACTIVATED_CHANCE)
trigger = Trigger.valueOf(nbt.getString(DataKeys.HABITAT_ACTIVATED_TRIGGER) ?: Trigger.REDSTONE.name)
cancelledNaturalSpawningRange = nbt.getInt(DataKeys.HABITAT_ACTIVATED_CANCEL_RANGE)
spawnRange = nbt.getInt(DataKeys.HABITAT_ACTIVATED_SPAWN_RANGE)
maxSpawns = nbt.getInt(DataKeys.HABITAT_ACTIVATED_MAX_SPAWNS)
maxSpawnsPerActivation = nbt.getInt(DataKeys.HABITAT_ACTIVATED_MAX_SPAWNS_PER_ACTIVATION)
pool = HabitatSpawningStyle.readPoolFromNBT(
nbt = nbt.getCompound(DataKeys.HABITAT_POOL),
poolInitializer = ::ActivatedHabitatPool,
spawnInitializer = ::ActivatedHabitatSpawn,
defaultPool = ActivatedHabitatPool::default
)
}
fun generateSpawnDetails(world: ServerLevel, pos: BlockPos) {
spawnDetails = pool.createSpawnDetails(habitatBlockEntity)
spawner = FixedAreaSpawner(
name = "habitat_spawner_$pos",
world = world,
position = pos,
spawnPool = SpawnPool("habitat_block").also {
it.details.addAll(spawnDetails)
it.precalculate()
},
maxPokemonPerChunk = 16F,
verticalRadius = spawnRange,
horizontalRadius = spawnRange
)
spawner.influences.add(ActivatedHabitatSpawningInfluence(this))
}
fun activate(world: ServerLevel, pos: BlockPos) {
// Check if the spawn attempt should even happen based on chance.
if (chance < 1F && world.random.nextFloat() >= chance) {
return
} else if (spawnDetails.isEmpty()) {
// If there are no spawn details, then there's no point in trying to spawn anything.
return
}
// Check if the max spawns has been reached.
if (maxSpawns != -1) {
val existingSpawns = habitatBlockEntity.spawnedEntityIDs.size
if (existingSpawns >= maxSpawns) {
return
}
}
val cause = SpawnCause(
spawner = spawner,
entity = world.getNearestPlayer(
pos.x.toDouble(),
pos.y.toDouble(),
pos.z.toDouble(),
64.0,
true
)
)
val remainingUntilMaxSpawns = if (maxSpawns == -1) Int.MAX_VALUE else maxSpawns - habitatBlockEntity.spawnedEntityIDs.size
val maxSpawnsForActivation = if (maxSpawnsPerActivation == -1) Int.MAX_VALUE else maxSpawnsPerActivation
val maxSpawns = minOf(remainingUntilMaxSpawns, maxSpawnsForActivation)
val event = HabitatSpawnActivatedEvent(
habitatBlockEntity = habitatBlockEntity,
activatedHabitatSpawning = this,
cause = cause,
maxSpawns = maxSpawns
)
CobblemonEvents.HABITAT_SPAWN_ACTIVATED.postThen(event = event) {
spawner.run(
cause = event.cause,
maxSpawns = event.maxSpawns
)
}
}
}
@@ -0,0 +1,79 @@
/*
* 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.habitats.spawningstyle
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.habitats.HabitatPool
import com.cobblemon.mod.common.api.habitats.HabitatPools
import com.cobblemon.mod.common.api.habitats.HabitatSpawn
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.Tag
import net.minecraft.resources.ResourceLocation
/**
* Holds habitat block specific details about the methodology behind how the habitat block will spawn Pokémon. This is
* either an [ActivatedHabitatSpawning] or a [NaturalHabitatSpawning].
*
* @see ActivatedHabitatSpawning
* @see NaturalHabitatSpawning
* @author Hiroku
* @since February 14th, 2026
*/
sealed interface HabitatSpawningStyle {
val type: ResourceLocation
fun writeToNBT(nbt: CompoundTag)
fun readFromNBT(nbt: CompoundTag)
fun writePoolToNBT(pool: HabitatPool<*>, nbt: CompoundTag) {
nbt.putString(DataKeys.HABITAT_POOL_ID, pool.id.toString())
if (pool.id.path.startsWith("custom__")) {
nbt.putString(DataKeys.HABITAT_POOL_NAME, pool.name)
val spawnsListNBT = ListTag()
for (spawn in pool.spawns) {
val spawnTag = CompoundTag()
spawn.writeToNBT(spawnTag)
spawnsListNBT.add(spawnTag)
}
nbt.put(DataKeys.HABITAT_POOL_SPAWNS, spawnsListNBT)
}
}
companion object {
fun <T : HabitatSpawn, P : HabitatPool<T>> readPoolFromNBT(nbt: CompoundTag, poolInitializer: () -> P, spawnInitializer: () -> T, defaultPool: () -> P): P {
var pool: P
val poolId = nbt.getString(DataKeys.HABITAT_POOL_ID).asIdentifierDefaultingNamespace()
if (nbt.contains(DataKeys.HABITAT_POOL_NAME)) {
// It's one that was stored on the block
pool = poolInitializer()
pool.id = poolId
pool.name = nbt.getString(DataKeys.HABITAT_POOL_NAME)
val spawnsListNBT = nbt.getList(DataKeys.HABITAT_POOL_SPAWNS, Tag.TAG_COMPOUND.toInt())
val spawns = mutableListOf<T>()
spawnsListNBT.forEach { spawnTag ->
try {
val spawn = spawnInitializer().apply { readFromNBT(spawnTag as CompoundTag) }
spawns.add(spawn)
} catch (_: Exception) {
// If there's an error reading a spawn, skip it and log the error.
Cobblemon.LOGGER.error("Error reading activated habitat spawn from NBT for habitat block, skipping.")
}
}
pool.spawns = spawns
} else {
pool = HabitatPools.habitatPoolsById[poolId] as? P ?: defaultPool()
}
return pool
}
}
}
@@ -0,0 +1,75 @@
/*
* 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.habitats.spawningstyle
import com.cobblemon.mod.common.api.habitats.NaturalHabitatPool
import com.cobblemon.mod.common.api.habitats.NaturalHabitatSpawn
import com.cobblemon.mod.common.api.habitats.spawningstyle.HabitatSpawningStyle.Companion.readPoolFromNBT
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.util.DataKeys
import net.minecraft.nbt.CompoundTag
import net.minecraft.resources.ResourceLocation
/**
* A habitat spawning style defined by tapping into regular spawning methods. This works by adding to the spawn list
* for spawning that's occurring nearby. When [replaceSpawns] is set to true, the original spawns will be
* removed completely in favour of the spawns provided in this habitat's [pool].
*
* The spawn details are generated from the [pool] every time there is some change to the habitat block's settings,
* then those spawn details are used for spawning Pokémon when world spawning is occurring nearby.
*
* Whether or not this interactions with overworld or fishing spawns is determined by whether the habitat pool has
* overworld or fishing spawns respectively.
*
* @author Hiroku
* @since February 22nd, 2026
*/
class NaturalHabitatSpawning(val habitatBlockEntity: HabitatBlockEntity) : HabitatSpawningStyle {
@Transient
override val type: ResourceLocation = NaturalHabitatSpawn.TYPE
var pool: NaturalHabitatPool = NaturalHabitatPool.default()
var replaceSpawns = false
var rangeOfInfluence = 16
@Transient
var affectsFishing = false
@Transient
var affectsOverworld = false
@Transient
var spawnDetails: List<SpawnDetail> = listOf()
fun generateSpawnDetails() {
spawnDetails = pool.createSpawnDetails(habitatBlockEntity = habitatBlockEntity)
affectsFishing = spawnDetails.any { it.spawnablePositionType.name == "fishing" }
affectsOverworld = spawnDetails.any { it.spawnablePositionType.name != "fishing" }
}
override fun writeToNBT(nbt: CompoundTag) {
nbt.putBoolean(DataKeys.HABITAT_NATURAL_REPLACE_SPAWNS, replaceSpawns)
nbt.putInt(DataKeys.HABITAT_NATURAL_RANGE_OF_INFLUENCE, rangeOfInfluence)
val poolNBT = CompoundTag()
writePoolToNBT(pool, poolNBT)
nbt.put(DataKeys.HABITAT_POOL, poolNBT)
}
override fun readFromNBT(nbt: CompoundTag) {
replaceSpawns = nbt.getBoolean(DataKeys.HABITAT_NATURAL_REPLACE_SPAWNS)
rangeOfInfluence = nbt.getInt(DataKeys.HABITAT_NATURAL_RANGE_OF_INFLUENCE)
pool = readPoolFromNBT(
nbt = nbt.getCompound(DataKeys.HABITAT_POOL),
poolInitializer = ::NaturalHabitatPool,
spawnInitializer = ::NaturalHabitatSpawn,
defaultPool = NaturalHabitatPool::default
)
}
}
@@ -9,7 +9,7 @@
package com.cobblemon.mod.common.api.net
/**
* This marks a packet as "unsplitable". This is used so neoforge doesnt destroy our buffers when encoding/decoding
* This marks a packet as "unsplittable". This is used so neoforge doesnt destroy our buffers when encoding/decoding
* PokemonDTOs (We allocate buffers, and it kinda breaks when the same instance of PokemonDTO is encoded more than once
*
* Technically, even if Neo didnt encode the same packet twice, we wouldnt want to split PokemonDTOs.
@@ -61,7 +61,7 @@ object NPCClasses : JsonDataRegistry<NPCClass> {
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(TimeRange::class.java, IntRangesAdapter(TimeRange.timeRanges) { TimeRange(*it) })
.registerTypeAdapter(TimeRange::class.java, TimeRange.adapter)
.registerTypeAdapter(ItemDropMethod::class.java, ItemDropMethod.adapter)
.registerTypeAdapter(SleepDepth::class.java, SleepDepth.adapter)
.registerTypeAdapter(DropEntry::class.java, DropEntryAdapter)
@@ -57,7 +57,7 @@ object NPCPresets : JsonDataRegistry<NPCPreset> {
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(TimeRange::class.java, IntRangesAdapter(TimeRange.timeRanges) { TimeRange(*it) })
.registerTypeAdapter(TimeRange::class.java, TimeRange.adapter)
.registerTypeAdapter(ItemDropMethod::class.java, ItemDropMethod.adapter)
.registerTypeAdapter(SleepDepth::class.java, SleepDepth.adapter)
.registerTypeAdapter(DropEntry::class.java, DropEntryAdapter)
@@ -113,7 +113,7 @@ object PokemonSpecies : JsonDataRegistry<Species> {
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(TimeRange::class.java, IntRangesAdapter(TimeRange.timeRanges) { TimeRange(*it) })
.registerTypeAdapter(TimeRange::class.java, TimeRange.adapter)
.registerTypeAdapter(ItemDropMethod::class.java, ItemDropMethod.adapter)
.registerTypeAdapter(SleepDepth::class.java, SleepDepth.adapter)
.registerTypeAdapter(DropEntry::class.java, DropEntryAdapter)
@@ -1,15 +0,0 @@
package com.cobblemon.mod.common.api.spawning.grotto
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import net.minecraft.core.BlockPos
import net.minecraft.world.entity.Entity
class GrottoSpawnCause(
spawner: Spawner,
bucket: SpawnBucket,
entity: Entity? = null,
val structureCenter: BlockPos,
val grottoPos: BlockPos
) : SpawnCause(spawner, bucket, entity)
@@ -17,8 +17,8 @@ import com.cobblemon.mod.common.api.npc.NPCClass
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.api.spawning.position.SpawnablePositionType
import com.cobblemon.mod.common.api.spawning.detail.PossibleHeldItem
import com.cobblemon.mod.common.api.spawning.position.SpawnablePositionType
import com.cobblemon.mod.common.api.spawning.preset.SpawnDetailPreset
import com.cobblemon.mod.common.util.adapters.*
import com.cobblemon.mod.common.util.cobblemonResource
@@ -27,12 +27,12 @@ import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.mojang.datafixers.util.Either
import net.minecraft.core.registries.Registries
import net.minecraft.world.level.block.Block
import net.minecraft.tags.TagKey
import net.minecraft.server.packs.PackType
import net.minecraft.server.level.ServerPlayer
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.tags.TagKey
import net.minecraft.world.level.biome.Biome
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.levelgen.structure.Structure
import net.minecraft.world.level.material.Fluid
@@ -68,7 +68,7 @@ object SpawnDetailPresets : JsonDataRegistry<SpawnDetailPreset> {
.registerTypeAdapter(SpawnDetailPreset::class.java, SpawnDetailPresetAdapter)
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(SpawningCondition::class.java, SpawningConditionAdapter)
.registerTypeAdapter(TimeRange::class.java, IntRangesAdapter(TimeRange.timeRanges) { TimeRange(*it) })
.registerTypeAdapter(TimeRange::class.java, TimeRange.adapter)
.registerTypeAdapter(MoonPhaseRange::class.java, IntRangesAdapter(MoonPhaseRange.moonPhaseRanges) { MoonPhaseRange(*it) })
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
.registerTypeAdapter(PossibleHeldItem::class.java, PossibleHeldItemAdapter)
@@ -16,9 +16,9 @@ import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.npc.NPCClass
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.api.spawning.position.SpawnablePositionType
import com.cobblemon.mod.common.api.spawning.detail.PossibleHeldItem
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.position.SpawnablePositionType
import com.cobblemon.mod.common.util.adapters.*
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
@@ -63,7 +63,7 @@ object SpawnLoader {
.registerTypeAdapter(SpawnDetail::class.java, SpawnDetailAdapter)
.registerTypeAdapter(DropEntry::class.java, DropEntryAdapter)
.registerTypeAdapter(SpawningCondition::class.java, SpawningConditionAdapter)
.registerTypeAdapter(TimeRange::class.java, IntRangesAdapter(TimeRange.timeRanges) { TimeRange(*it) })
.registerTypeAdapter(TimeRange::class.java, TimeRange.adapter)
.registerTypeAdapter(MoonPhaseRange::class.java, IntRangesAdapter(MoonPhaseRange.moonPhaseRanges) { MoonPhaseRange(*it) })
.registerTypeAdapter(ItemDropMethod::class.java, ItemDropMethod.adapter)
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.api.spawning.TimeRange.Companion.timeRanges
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.util.adapters.IntRangesAdapter
/**
* A range of time ticks for a world mainly to be used in [SpawningCondition]s. A time range
@@ -43,6 +44,7 @@ class TimeRange : IntRanges {
"dusk" to TimeRange(11834..13701),
"twilight" to TimeRange(11834..13701, 22300..23999, 0..166)
)
val adapter = IntRangesAdapter(timeRanges) { TimeRange(*it) }
}
constructor() : super()
@@ -1,39 +0,0 @@
package com.cobblemon.mod.common.api.spawning.influence
import net.minecraft.resources.ResourceLocation
class GrottoInfluence(
private val structureId: ResourceLocation,
private val allowedSpawns: List<ResourceLocation>? = null // null = allow all from habitat
) : SpawningInfluence {
/*companion object {
private val LOGGER = LoggerFactory.getLogger("GrottoInfluence")
}
override fun affectSpawnable(detail: SpawnDetail, ctx: SpawningContext): Boolean {
if (detail !is PokemonSpawnDetail) return true
val detailId = ResourceLocation.tryParse(detail.pokemon.species!!) ?: return false
// If strict filtering enabled, reject anything not in allowed list
if (allowedSpawns != null && !allowedSpawns.contains(detailId)) {
LOGGER.info("[GrottoInfluence] Rejecting ${detailId.path} — not in current moon phase group.")
return false
}
return true
}
override fun affectWeight(detail: SpawnDetail, ctx: SpawningContext, weight: Float): Float {
if (detail !is PokemonSpawnDetail) return weight
val detailId = ResourceLocation.tryParse(detail.pokemon.species!!) ?: return weight
val boostTargetList = allowedSpawns ?: Habitats.getHabitat(structureId)?.spawnPool?.flatten() ?: emptyList()
return if (boostTargetList.contains(detailId)) {
LOGGER.info("[GrottoInfluence] Boosting spawn weight for ${detailId.path} (structure: $structureId)")
weight * 100f
} else {
weight
}
}*/
}
@@ -1,30 +0,0 @@
/*
* 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.spawning.influence
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import kotlin.math.sqrt
import net.minecraft.core.BlockPos
/**
* A type of [WorldSlicedSpawningInfluence] that is only applied to contexts within a particular radius
* of a position.
*
* @author Hiroku
* @since March 9th, 2025
*/
class WorldSlicedSpatialSpawningInfluence(
val pos: BlockPos,
val radius: Float,
influence: SpawningInfluence
) : WorldSlicedSpawningInfluence(influence) {
override fun appliesTo(context: SpawningContext): Boolean {
return sqrt(context.position.distSqr(pos)) <= radius
}
}
@@ -1,23 +0,0 @@
/*
* 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.spawning.influence
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
/**
* A wrapping around a [SpawningInfluence] that was produced for a [WorldSlice]. This is wrapped
* to provide a filtration mechanism on which of these can be applied to a [SpawningContext].
*
* @author Hiroku
* @since March 9th, 2025
*/
open class WorldSlicedSpawningInfluence(val influence: SpawningInfluence) {
/** Whether this influence will apply to */
open fun appliesTo(context: SpawningContext): Boolean = true
}
@@ -1,56 +0,0 @@
/*
* 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/.
*/
///*
// * 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.spawning.influence.detector
//
//import com.cobblemon.mod.common.CobblemonPoiTypes
//import com.cobblemon.mod.common.api.spawning.influence.GrottoImpactedSpawningInfluence
//import com.cobblemon.mod.common.api.spawning.influence.UnconditionalZoneSpawningInfluence
//import com.cobblemon.mod.common.api.spawning.influence.ZoneSpawningInfluence
//import com.cobblemon.mod.common.api.spawning.spawner.Spawner
//import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
//import com.cobblemon.mod.common.util.toBlockPos
//import net.minecraft.core.BlockPos
//import net.minecraft.core.Holder
//import net.minecraft.server.level.ServerLevel
//import net.minecraft.world.entity.ai.village.poi.PoiManager
//import net.minecraft.world.entity.ai.village.poi.PoiType
//import net.minecraft.world.level.block.state.BlockState
//
//object GrottoBlockDetector : SpawningInfluenceDetector {
// @JvmField
// val RANGE: Int = 48
//
// override fun detectFromInput(spawner: Spawner, input: SpawningZoneInput): List<ZoneSpawningInfluence> {
// val world = input.world
// val searchRange = maxOf(RANGE, input.length * 2, input.height * 2)
// val grottoBlockPositions = world.poiManager.findAll(
// { holder: Holder<PoiType> -> holder.`is`(CobblemonPoiTypes.GROTTO_BLOCK_KEY) },
// { true },
// input.getCenter().toBlockPos(),
// searchRange,
// PoiManager.Occupancy.ANY
// ).toList()
//
// return if (grottoBlockPositions.isEmpty()) {
// emptyList()
// } else {
// listOf(UnconditionalZoneSpawningInfluence(influence = GrottoImpactedSpawningInfluence()))
// }
// }
//
// override fun detectFromBlock(world: ServerLevel, pos: BlockPos, blockState: BlockState) = emptyList<ZoneSpawningInfluence>()
//}
@@ -0,0 +1,55 @@
/*
* 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.spawning.influence.detector
import com.cobblemon.mod.common.CobblemonPoiTypes
import com.cobblemon.mod.common.api.spawning.influence.ConditionalSpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningZoneInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.util.math.pow
import com.cobblemon.mod.common.util.toBlockPos
import net.minecraft.core.BlockPos
import net.minecraft.core.Holder
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.ai.village.poi.PoiManager
import net.minecraft.world.entity.ai.village.poi.PoiType
import net.minecraft.world.level.block.state.BlockState
object HabitatBlockDetector : SpawningInfluenceDetector {
const val RANGE = 128
override fun detectFromBlock(world: ServerLevel, pos: BlockPos, blockState: BlockState) = emptyList<SpawningZoneInfluence>()
override fun detectFromInput(spawner: Spawner, input: SpawningZoneInput): List<SpawningZoneInfluence> {
val world = input.world
val searchRange = maxOf(RANGE, input.length * 2, input.height * 2)
val habitatBlockPositions = world.poiManager.findAll(
{ holder: Holder<PoiType> -> holder.`is`(CobblemonPoiTypes.HABITAT_BLOCK_KEY) },
{ true },
input.getCenter().toBlockPos(),
searchRange,
PoiManager.Occupancy.ANY
).toList()
// just fyi, HabitatBlockEntity implements SpawningInfluence
return habitatBlockPositions
.mapNotNull { pos -> world.getBlockEntity(pos) as? HabitatBlockEntity }
.filter { habitatBlockEntity -> habitatBlockEntity.getInfluentialRange(spawner) > 0 }
.map {
val range = it.getInfluentialRange(spawner)
object : ConditionalSpawningZoneInfluence {
override val influence = it
override fun appliesTo(spawnablePosition: SpawnablePosition) = spawnablePosition.position.distSqr(it.blockPos) <= range pow 2
}
}
}
}
@@ -29,9 +29,10 @@ import net.minecraft.world.level.block.state.BlockState
interface SpawningInfluenceDetector {
companion object {
@JvmStatic
val detectors = mutableSetOf<SpawningInfluenceDetector>(
val detectors = mutableSetOf(
SaccharineLogSlatheredDetector,
IncenseSweetDetector
IncenseSweetDetector,
HabitatBlockDetector,
)
}
@@ -1,80 +0,0 @@
/*
* 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.spawning.prospecting
import com.cobblemon.mod.common.api.spawning.influence.WorldSlicedSpatialSpawningInfluence
import com.cobblemon.mod.common.api.spawning.influence.WorldSlicedSpawningInfluence
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.block.state.BlockState
object GrottoBlockProspector : SpawningInfluenceProspector {
@JvmField
val RANGE: Int = 48
// todo going with this prospect method for now since the GrottoSpawner might be what I want instead of handling it here
override fun prospect(spawner: Spawner, area: SpawningArea): MutableList<WorldSlicedSpawningInfluence> {
return mutableListOf() // GrottoSpawner handles spawning directly now
}
// todo this was some test code of a prospect function that would try to alter the already filtered spawn lists, but I doubt we want to go this direction
/*override fun prospect(spawner: Spawner, area: SpawningArea): MutableList<WorldSlicedSpawningInfluence> {
val world = area.world
val listOfInfluences = mutableListOf<WorldSlicedSpawningInfluence>()
val searchRange = RANGE + ceil(sqrt(((area.length pow 2) + (area.width pow 2)).toDouble())).toInt()
val grottoBlockPositions = world.poiManager.findAll({ holder: Holder<PoiType> -> holder.`is`(CobblemonPoiTypes.GROTTO_BLOCK_KEY) },{ true },area.getCenter().toBlockPos(),searchRange, PoiManager.Occupancy.ANY).toList()
for (grottoBlockPos in grottoBlockPositions) {
val blockState = world.getBlockState(grottoBlockPos)
if (blockState.block !is GrottoBlock) continue
val blockEntity = world.getBlockEntity(grottoBlockPos) as? GrottoBlockEntity ?: continue
val structureId = blockEntity.detectedStructure ?: continue
if (structureId == null) {
println("[GrottoBlockProspector] Grotto at $grottoBlockPos has no detected structure.")
continue
}
val habitat = Habitats.getHabitat(structureId)
if (habitat == null) {
println("[GrottoBlockProspector] Grotto at $grottoBlockPos references unknown habitat: $structureId")
continue
}
println("[GrottoBlockProspector] Found Grotto at $grottoBlockPos")
println(" ↳ Structure: $structureId")
if (habitat.spawnPool.isEmpty()) {
println(" ↳ Spawn pool is empty.")
} else {
println(" ↳ Spawn pool:")
habitat.spawnPool.forEach {
println(" • $it")
}
}
val allowedSpawns = blockEntity.currentSpawnGroup
if (allowedSpawns.isNotEmpty()) {
listOfInfluences.add(
WorldSlicedSpatialSpawningInfluence(
grottoBlockPos,
RANGE.toFloat(),
influence = GrottoInfluence(structureId, allowedSpawns)
)
)
}
}
return listOfInfluences
}*/
override fun prospectBlock(world: ServerLevel, pos: BlockPos, blockState: BlockState): WorldSlicedSpatialSpawningInfluence? { return null }
}
@@ -1,37 +0,0 @@
/*
* 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.spawning.prospecting
import com.cobblemon.mod.common.api.spawning.influence.WorldSlicedSpawningInfluence
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.SpawningArea
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.block.state.BlockState
/**
* Prospects for [WorldSlicedSpawningInfluence] in a world at a given position. Occurs as part of a
* [SpawningProspector] scanning a part of the world while formulating a [WorldSlice].
*
* @author Hiroku
* @since March 9th, 2025
*/
interface SpawningInfluenceProspector {
companion object {
@JvmStatic
val prospectors = mutableSetOf<SpawningInfluenceProspector>(
// todo commenting out the GrottoBlockProspector since I don't know if we want to use it at all anyways
//GrottoBlockProspector
)
}
fun prospect(spawner: Spawner, area: SpawningArea) : MutableList<WorldSlicedSpawningInfluence>
fun prospectBlock(world: ServerLevel, pos: BlockPos, blockState: BlockState): WorldSlicedSpawningInfluence?
}
@@ -8,21 +8,27 @@
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.CobblemonPoiTypes
import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnRules
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.BucketMultiplyingInfluence
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.influence.detector.HabitatBlockDetector
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FishingSpawnerFactory.buildPositionInfluences
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.util.mutableLazy
import net.minecraft.core.BlockPos
import net.minecraft.core.Holder
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.ai.village.poi.PoiManager
import net.minecraft.world.entity.ai.village.poi.PoiType
import net.minecraft.world.item.ItemStack
import com.cobblemon.mod.common.api.spawning.BestSpawner;
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
/**
@@ -57,9 +63,10 @@ object FishingSpawnerFactory {
* List of influences for a new cast of the fishing rod. Context is provided at
* build time in [buildPositionInfluences]
*/
var positionInfluenceBuilders = mutableListOf<(ctx: Context) -> SpawningInfluence?>(
{ ctx -> PlayerLevelRangeInfluence(ctx.player, TYPICAL_VARIATION) },
{ ctx -> BucketNormalizingInfluence(tier = ctx.lureLevel + ctx.luckOfTheSeaLevel) }
var positionInfluenceBuilders = mutableListOf<(ctx: Context) -> List<SpawningInfluence>>(
{ ctx -> listOf(PlayerLevelRangeInfluence(ctx.player, TYPICAL_VARIATION)) },
{ ctx -> listOf(BucketNormalizingInfluence(tier = ctx.lureLevel + ctx.luckOfTheSeaLevel)) },
{ ctx -> findHabitatInfluences(ctx.world, ctx.pos, ctx.spawner).toList() }
)
fun createSharedSpawner(): BasicSpawner {
@@ -77,10 +84,29 @@ object FishingSpawnerFactory {
}
}
fun findHabitatInfluences(world: ServerLevel, position: BlockPos, spawner: Spawner): Array<HabitatBlockEntity> {
val habitatBlockPositions = world.poiManager.findAll(
{ holder: Holder<PoiType> -> holder.`is`(CobblemonPoiTypes.HABITAT_BLOCK_KEY) },
{ true },
position,
HabitatBlockDetector.RANGE,
PoiManager.Occupancy.ANY
).toList()
// just fyi, HabitatBlockEntity implements SpawningInfluence
val influentialHabitats = habitatBlockPositions
.mapNotNull { pos -> world.getBlockEntity(pos) as? HabitatBlockEntity }
.filter { habitatBlockEntity -> habitatBlockEntity.getInfluentialRange(spawner) > 0 }
.toTypedArray()
return influentialHabitats
}
/**
* Builds the per-cast influences for [FishingSpawnablePosition]
*/
fun buildPositionInfluences(ctx: Context): MutableList<SpawningInfluence> {
return positionInfluenceBuilders.mapNotNullTo(mutableListOf()) { it(ctx) }
return positionInfluenceBuilders.asSequence().flatMap { it.invoke(ctx) }.toMutableList()
}
}
@@ -1,225 +0,0 @@
package com.cobblemon.mod.common.api.spawning.spawner
import com.cobblemon.mod.common.CobblemonPoiTypes
import com.cobblemon.mod.common.api.habitats.Habitats
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
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.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.grotto.GrottoSpawnCause
import com.cobblemon.mod.common.api.spawning.selection.SpawningSelector
import com.cobblemon.mod.common.block.entity.GrottoBlockEntity
import com.cobblemon.mod.common.util.party
import com.cobblemon.mod.common.util.server
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max
import kotlin.math.min
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.ai.village.poi.PoiManager
class GrottoSpawner(
name: String,
fallbackSpawns: SpawnPool,
manager: SpawnerManager
) : AreaSpawner(name, fallbackSpawns, manager) {
companion object {
private val cache = ConcurrentHashMap<BlockPos, Pair<SpawnPool, Long>>()
private const val CACHE_DURATION_MS = 10_000
}
override var ticksBetweenSpawns: Float = 120F
// todo use this getArea to try to use a new GrottoSpawnCause and use that to send the structure center so spawns are around the structures
override fun getArea(cause: SpawnCause): SpawningArea? {
if (cause is GrottoSpawnCause) {
val center = cause.structureCenter
return SpawningArea(
cause = cause,
world = server()?.getLevel(cause.entityWorldId) ?: return null,
baseX = center.x - 8,
baseY = center.y - 4,
baseZ = center.z - 8,
length = 16,
height = 8,
width = 16
)
}
return null
}
override fun run(cause: SpawnCause): Pair<SpawningContext, SpawnDetail>? {
//println("[GrottoSpawner] run() starting for cause: $cause")
val area = getArea(cause)
//println("[GrottoSpawner] getArea() result: $area")
val constrainedArea = if (area != null) constrainArea(area) else null
//println("[GrottoSpawner] constrainArea() result: $constrainedArea")
if (constrainedArea != null) {
val slice = prospector.prospect(this, constrainedArea)
val contexts = resolver.resolve(this, contextCalculators, slice)
return getSpawningSelector().select(this, contexts)
}
//println("[GrottoSpawner] Area is null or invalid, skipping spawn")
return null
}
// todo test tick
override fun tick() {
super.tick()
ticksUntilNextSpawn -= tickTimerMultiplier
if (ticksUntilNextSpawn > 0) return
val server = server() ?: return
val world = server.overworld() ?: return
for (player in world.players()) {
val searchOrigin = player.blockPosition()
val poiManager = world.poiManager
val grottoPOIs = poiManager.findAll(
{ it.`is`(CobblemonPoiTypes.GROTTO_BLOCK_KEY) },
{ true },
searchOrigin,
128, // Search radius
PoiManager.Occupancy.ANY
)
for (grottoPos in grottoPOIs) {
val blockEntity = world.getBlockEntity(grottoPos) as? GrottoBlockEntity ?: continue
val structureCenter = blockEntity.structureCenter ?: continue
val cause = GrottoSpawnCause(this, chooseBucket(), player, structureCenter, grottoPos)
val result = run(cause)
//println("[GrottoSpawner] Grotto at $grottoPos spawn result: $result")
if (result != null) {
val (context, detail) = result
val action = detail.doSpawn(context)
//println("[GrottoSpawner] doSpawn: $detail -> $action")
val completion = action.complete()
//println("[GrottoSpawner] spawn complete: $completion")
}
}
}
ticksUntilNextSpawn = ticksBetweenSpawns
}
// todo we want to make it so that it ignores conditions of the area and just use the SpawnPool of the JSON registry that has been cycled during that moon cycle
override fun getSpawningSelector(): SpawningSelector {
return object : SpawningSelector {
override fun select(spawner: Spawner, contexts: List<SpawningContext>): Pair<SpawningContext, SpawnDetail>? {
val pool = when {
spawner is GrottoSpawner && contexts.firstOrNull()?.cause is GrottoSpawnCause ->
spawner.getSpawnPool(contexts.first().cause)
else -> spawner.getSpawnPool()
}
for (ctx in contexts) {
val details = pool.details.filterIsInstance<PokemonSpawnDetail>()
if (details.isEmpty()) continue
val chosen = weightedRandom(details) { it.weight.takeIf { w -> w > 0 } ?: 1.0f }
//println("[GrottoSpawner] Selected ${chosen.pokemon.species} for spawn")
return ctx to chosen
}
//println("[GrottoSpawner] No spawnable Pokémon found.")
return null
}
override fun getTotalWeights(spawner: Spawner, contexts: List<SpawningContext>): Map<SpawnDetail, Float> {
return spawner.getSpawnPool().details.associateWith { it.weight.takeIf { w -> w > 0 } ?: 1.0f }
}
private fun <T> weightedRandom(entries: List<T>, weightFn: (T) -> Float): T {
val totalWeight = entries.sumOf { weightFn(it).toDouble() }
var roll = Math.random() * totalWeight
for (entry in entries) {
roll -= weightFn(entry)
if (roll <= 0) return entry
}
return entries.last()
}
}
}
override fun getSpawnPool(): SpawnPool {
return spawns
}
// todo new getSpawnPool
fun getSpawnPool(cause: SpawnCause): SpawnPool {
val server = server() ?: return spawns
val world = server.overworld() ?: return spawns
val now = System.currentTimeMillis()
if (cause !is GrottoSpawnCause) return spawns
val center = cause.grottoPos
val targetEntity = world.getChunk(center)?.getBlockEntity(center) as? GrottoBlockEntity ?: return spawns
val structure = targetEntity.detectedStructure ?: return spawns
val spawnGroup = targetEntity.currentSpawnGroup
if (spawnGroup.isEmpty()) return spawns
val cached = cache[center]
if (cached != null && now - cached.second <= CACHE_DURATION_MS) {
println("[GrottoSpawner] Cache hit at $center — using group: $spawnGroup")
return cached.first
}
val habitat = Habitats.getHabitat(structure)
if (habitat == null) {
println("[GrottoSpawner] No habitat found for structure: $structure")
return spawns
}
val selectedGroup = habitat.spawnPool.find { group ->
group.spawns.toSet() == spawnGroup.toSet()
} ?: return spawns.also {
/*println("[GrottoSpawner] Could not match spawn group at $center to any group in habitat: $structure")
println(" ↳ Current spawnGroup: $spawnGroup")
println(" ↳ Habitat groups:")
habitat.spawnPool.forEachIndexed { i, g ->
println(" [$i]: ${g.spawns}")
}*/
}
val poolCacheName = "grotto_pool_${center.x}_${center.y}_${center.z}"
val pool = SpawnPool(poolCacheName)
for (entry in selectedGroup.spawns) {
val player = cause.entity as? ServerPlayer
val party = player?.party()?.filterNotNull()
val maxLevel = party?.maxOfOrNull { it.level } ?: 20
val variation = 5
val minLevel = max(1, maxLevel - variation)
val maxLevelFinal = min(100, maxLevel + variation)
val level = (minLevel..maxLevelFinal).random()
val detail = PokemonSpawnDetail().apply {
pokemon.species = entry.species.toString()
weight = entry.weight.toFloat()
pokemon.level = level
}
pool.details.add(detail)
}
/*println("[GrottoSpawner] Constructing pool for Grotto at ${cause.structureCenter}")
println(" ↳ Detected structure: $structure")
println(" ↳ Spawn group: $spawnGroup")
println(" ↳ Selected group: $selectedGroup")
println(" ↳ Pool size: ${pool.details.size}")*/
cache[center] = pool to now
return pool
}
}
@@ -50,6 +50,7 @@ object CobblemonBlockTags {
@JvmField val FOSSIL_MACHINE_PARTS = createTag("fossil_machine_parts")
@JvmField val GALARICA_NUT_MAY_PLACE_ON = createTag("galarica_nut_may_place_on")
@JvmField val GEMSTONES = createTag("gemstones")
@JvmField val HABITAT_MIMICS = createTag("habitat_mimics")
@JvmField val GILDED_CHESTS = createTag("gilded_chests")
@JvmField val GLAZED_TERRACOTTA_BLOCKS = createTag("glazed_terracotta_blocks")
@JvmField val ICE_STONE_ORES = createTag("ice_stone_ores")
@@ -1,257 +0,0 @@
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.api.habitats.Habitats
import com.cobblemon.mod.common.api.habitats.WeightedSpecies
import com.cobblemon.mod.common.block.GrottoBlock
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.Tag
import net.minecraft.network.chat.Component
import net.minecraft.network.protocol.Packet
import net.minecraft.network.protocol.game.ClientGamePacketListener
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
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.levelgen.structure.StructureStart
import net.minecraft.world.phys.BlockHitResult
class GrottoBlockEntity(pos: BlockPos, state: BlockState) :
BlockEntity(CobblemonBlockEntities.GROTTO_BLOCK, pos, state) {
var detectedStructure: ResourceLocation? = null
var structureCenter: BlockPos? = null
var hasCheckedStructure: Boolean = false
var lastMoonPhaseChecked: Int = -1
var currentSpawnGroup: List<WeightedSpecies> = emptyList()
var mimicId: ResourceLocation = BuiltInRegistries.BLOCK.getKey(Blocks.STONE)
val mimickedState: BlockState
get() = BuiltInRegistries.BLOCK.get(mimicId).defaultBlockState() ?: Blocks.STONE.defaultBlockState()
// todo to test with checking how it looks as other blocks
val mimicCycle: List<ResourceLocation> = listOf(
ResourceLocation.parse("minecraft:stone"),
ResourceLocation.parse("minecraft:cobblestone"),
ResourceLocation.parse("minecraft:granite"),
ResourceLocation.parse("minecraft:diorite"),
ResourceLocation.parse("minecraft:andesite"),
ResourceLocation.parse("minecraft:deepslate"),
ResourceLocation.parse("minecraft:polished_deepslate"),
ResourceLocation.parse("minecraft:oak_planks"),
ResourceLocation.parse("minecraft:spruce_planks"),
ResourceLocation.parse("minecraft:birch_planks"),
ResourceLocation.parse("minecraft:jungle_planks"),
ResourceLocation.parse("minecraft:acacia_planks"),
ResourceLocation.parse("minecraft:dark_oak_planks"),
ResourceLocation.parse("minecraft:mangrove_planks"),
ResourceLocation.parse("minecraft:cherry_planks"),
ResourceLocation.parse("minecraft:bamboo_planks"),
ResourceLocation.parse("minecraft:bricks"),
ResourceLocation.parse("minecraft:mossy_cobblestone"),
ResourceLocation.parse("minecraft:moss_block"),
ResourceLocation.parse("minecraft:bookshelf"),
ResourceLocation.parse("minecraft:loom"),
ResourceLocation.parse("minecraft:note_block"),
ResourceLocation.parse("minecraft:coal_block"),
ResourceLocation.parse("minecraft:iron_block"),
ResourceLocation.parse("minecraft:gold_block"),
ResourceLocation.parse("minecraft:diamond_block"),
ResourceLocation.parse("minecraft:emerald_block"),
ResourceLocation.parse("minecraft:lapis_block"),
ResourceLocation.parse("minecraft:redstone_block"),
ResourceLocation.parse("minecraft:netherite_block"),
ResourceLocation.parse("minecraft:obsidian"),
ResourceLocation.parse("minecraft:crying_obsidian"),
ResourceLocation.parse("minecraft:glowstone"),
ResourceLocation.parse("minecraft:blue_ice"),
ResourceLocation.parse("minecraft:packed_ice"),
ResourceLocation.parse("minecraft:ice"),
ResourceLocation.parse("minecraft:sandstone"),
ResourceLocation.parse("minecraft:red_sandstone"),
ResourceLocation.parse("minecraft:cut_sandstone"),
ResourceLocation.parse("minecraft:smooth_sandstone"),
ResourceLocation.parse("minecraft:black_concrete"),
ResourceLocation.parse("minecraft:white_concrete"),
ResourceLocation.parse("minecraft:lime_concrete"),
ResourceLocation.parse("minecraft:cyan_concrete"),
ResourceLocation.parse("minecraft:purple_concrete"),
ResourceLocation.parse("minecraft:light_blue_concrete"),
ResourceLocation.parse("minecraft:yellow_concrete"),
ResourceLocation.parse("minecraft:red_concrete")
)
var mimicIndex: Int = 0
fun setMimic(id: ResourceLocation) {
mimicId = id
setChanged()
level?.sendBlockUpdated(worldPosition, blockState, blockState, Block.UPDATE_ALL)
}
fun cycleMimic() {
mimicIndex = (mimicIndex + 1) % mimicCycle.size
setMimic(mimicCycle[mimicIndex])
}
fun onUse(player: Player, hit: BlockHitResult): InteractionResult {
cycleMimic()
player.displayClientMessage(Component.literal("Mimicking: $mimicId"), true)
return InteractionResult.SUCCESS
}
override fun saveAdditional(tag: CompoundTag, registryLookup: HolderLookup.Provider) {
super.saveAdditional(tag, registryLookup)
tag.putString("MimicId", mimicId.toString())
detectedStructure?.let { tag.putString("DetectedStructure", it.toString()) }
tag.putBoolean("HasCheckedStructure", hasCheckedStructure)
val list = ListTag()
currentSpawnGroup.forEach {
val entry = CompoundTag()
entry.putString("species", it.species.toString())
entry.putInt("weight", it.weight)
list.add(entry)
}
tag.put("CurrentSpawnGroup", list)
structureCenter?.let {
val centerTag = CompoundTag()
centerTag.putInt("X", it.x)
centerTag.putInt("Y", it.y)
centerTag.putInt("Z", it.z)
tag.put("StructureCenter", centerTag)
}
}
override fun loadAdditional(tag: CompoundTag, registryLookup: HolderLookup.Provider) {
super.loadAdditional(tag, registryLookup)
tag.getString("MimicId").let {
mimicId = ResourceLocation.tryParse(it) ?: BuiltInRegistries.BLOCK.getKey(Blocks.STONE)
}
hasCheckedStructure = tag.getBoolean("HasCheckedStructure")
detectedStructure =
tag.getString("DetectedStructure").takeIf { it.isNotEmpty() }?.let(ResourceLocation::tryParse)
val list = tag.getList("CurrentSpawnGroup", Tag.TAG_COMPOUND.toInt())
currentSpawnGroup = list.mapNotNull {
if (it !is CompoundTag) return@mapNotNull null
val species = ResourceLocation.tryParse(it.getString("species")) ?: return@mapNotNull null
val weight = it.getInt("weight")
WeightedSpecies(species, weight)
}
if (tag.contains("StructureCenter", Tag.TAG_COMPOUND.toInt())) {
val centerTag = tag.getCompound("StructureCenter")
val x = centerTag.getInt("X")
val y = centerTag.getInt("Y")
val z = centerTag.getInt("Z")
structureCenter = BlockPos(x, y, z)
}
}
override fun getUpdatePacket(): Packet<ClientGamePacketListener> {
return ClientboundBlockEntityDataPacket.create(this)
}
override fun getUpdateTag(registryLookup: HolderLookup.Provider): CompoundTag {
return saveWithoutMetadata(registryLookup)
}
fun updateLevelAndPos(level: Level, pos: BlockPos) {
val block = this.blockState.block
if (block is GrottoBlock) {
block.level = level
block.pos = pos
}
}
fun findNearbyStructure(level: ServerLevel, pos: BlockPos, radius: Int): ResourceLocation? {
val structureManager = level.structureManager()
val registry = level.registryAccess().registryOrThrow(Registries.STRUCTURE)
for (entry in registry.entrySet()) {
val structure = entry.value
val structureStart = structureManager.getStructureWithPieceAt(pos, structure)
if (structureStart != StructureStart.INVALID_START && structureStart.boundingBox.isInside(pos)) {
structureCenter = structureStart.boundingBox.center
return entry.key.location()
}
}
return null
}
companion object {
val TICKER = BlockEntityTicker<GrottoBlockEntity> { world, pos, state, blockEntity ->
if (world is ServerLevel) {
if (!blockEntity.hasCheckedStructure) {
blockEntity.hasCheckedStructure = true
val structure = blockEntity.findNearbyStructure(world, pos, radius = 5)
if (structure != null) {
blockEntity.detectedStructure = structure
blockEntity.setChanged()
world.sendBlockUpdated(pos, state, state, Block.UPDATE_CLIENTS)
println("[GrottoBlockEntity] Found structure at $pos: $structure")
}
}
val currentMoonPhase = world.moonPhase
if (currentMoonPhase != blockEntity.lastMoonPhaseChecked) {
blockEntity.lastMoonPhaseChecked = currentMoonPhase
if (currentMoonPhase == 7 || blockEntity.currentSpawnGroup.isEmpty()) {
val structure = blockEntity.detectedStructure
if (structure != null) {
blockEntity.detectedStructure = structure
blockEntity.setChanged()
world.sendBlockUpdated(pos, state, state, Block.UPDATE_CLIENTS)
println("[GrottoBlockEntity] Detected structure at $pos: $structure")
val habitat = Habitats.getHabitat(structure)
if (habitat != null && habitat.spawnPool.isNotEmpty()) {
val selectedGroup = weightedGroupRoll(habitat.spawnPool)
blockEntity.currentSpawnGroup = selectedGroup.spawns
blockEntity.lastMoonPhaseChecked = world.moonPhase
blockEntity.setChanged()
println("[GrottoBlockEntity] Selected spawn group for $structure: ${blockEntity.currentSpawnGroup.map { it.species }}")
}
}
}
}
}
}
private fun weightedGroupRoll(groups: List<com.cobblemon.mod.common.api.habitats.WeightedGroup>): com.cobblemon.mod.common.api.habitats.WeightedGroup {
val totalWeight = groups.sumOf { it.weight.toDouble() }
var roll = Math.random() * totalWeight
for (group in groups) {
roll -= group.weight
if (roll <= 0) return group
}
return groups.last() // fallback
}
}
}
@@ -1,21 +1,33 @@
package com.cobblemon.mod.common.block
/*
* 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.block.habitat
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.block.entity.GrottoBlockEntity
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.EntityBlock
import net.minecraft.world.level.block.HorizontalDirectionalBlock.FACING
import net.minecraft.world.level.block.RenderShape
import net.minecraft.world.level.block.SoundType
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BooleanProperty
import net.minecraft.world.level.storage.loot.LootParams
import net.minecraft.world.level.storage.loot.parameters.LootContextParams
import net.minecraft.world.phys.BlockHitResult
@@ -23,12 +35,35 @@ import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class GrottoBlock(properties: Properties) : Block(properties), EntityBlock {
class HabitatBlock(properties: Properties) : Block(properties), EntityBlock {
var level: Level? = null
var pos: BlockPos? = null
companion object {
val CANCELS_REGULAR_SPAWNS = BooleanProperty.create("cancels_regular_spawns")
val ACTIVATED_STYLE = BooleanProperty.create("activated_style")
}
init {
registerDefaultState(stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(CANCELS_REGULAR_SPAWNS, false)
.setValue(ACTIVATED_STYLE, true)
)
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
super.createBlockStateDefinition(builder)
builder.add(FACING, CANCELS_REGULAR_SPAWNS, ACTIVATED_STYLE)
}
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity {
return GrottoBlockEntity(pos, state)
return HabitatBlockEntity(pos, state)
}
override fun getStateForPlacement(blockPlaceContext: BlockPlaceContext): BlockState {
return defaultBlockState()
.setValue(FACING, blockPlaceContext.horizontalDirection.opposite)
}
override fun <T : BlockEntity> getTicker(
@@ -36,9 +71,9 @@ class GrottoBlock(properties: Properties) : Block(properties), EntityBlock {
state: BlockState,
type: BlockEntityType<T>
): BlockEntityTicker<T>? {
return if (!level.isClientSide && type == CobblemonBlockEntities.GROTTO_BLOCK) {
return if (!level.isClientSide && type == CobblemonBlockEntities.HABITAT_BLOCK) {
@Suppress("UNCHECKED_CAST")
GrottoBlockEntity.TICKER as BlockEntityTicker<T>
HabitatBlockEntity.TICKER as BlockEntityTicker<T>
} else {
null
}
@@ -46,14 +81,14 @@ class GrottoBlock(properties: Properties) : Block(properties), EntityBlock {
override fun useWithoutItem(state: BlockState, level: Level, pos: BlockPos, player: Player, hitResult: BlockHitResult): InteractionResult {
if (!level.isClientSide) {
val be = level.getBlockEntity(pos) as? GrottoBlockEntity ?: return InteractionResult.PASS
val be = level.getBlockEntity(pos) as? HabitatBlockEntity ?: return InteractionResult.PASS
return be.onUse(player, hitResult)
}
return InteractionResult.SUCCESS
}
override fun getDrops(state: BlockState, params: LootParams.Builder): MutableList<ItemStack> {
val blockEntity = params.getOptionalParameter(LootContextParams.BLOCK_ENTITY) as? GrottoBlockEntity
val blockEntity = params.getOptionalParameter(LootContextParams.BLOCK_ENTITY) as? HabitatBlockEntity
val player = params.getOptionalParameter(LootContextParams.THIS_ENTITY) as? Player
if (player?.isCreative == true) return mutableListOf()
@@ -71,10 +106,10 @@ class GrottoBlock(properties: Properties) : Block(properties), EntityBlock {
}
override fun getCollisionShape(
state: BlockState,
level: BlockGetter,
pos: BlockPos,
context: CollisionContext
state: BlockState,
level: BlockGetter,
pos: BlockPos,
context: CollisionContext
): VoxelShape {
return box(0.0, 0.0, 0.0, 16.0, 16.0, 16.0)
}
@@ -89,29 +124,33 @@ class GrottoBlock(properties: Properties) : Block(properties), EntityBlock {
}
override fun getOcclusionShape(state: BlockState, level: BlockGetter, pos: BlockPos): VoxelShape? {
return Shapes.block()
return Shapes.empty()
}
override fun useShapeForLightOcclusion(state: BlockState): Boolean {
return true
return false
}
override fun skipRendering(state: BlockState, adjacentBlockState: BlockState, side: Direction): Boolean {
return false
}
override fun spawnDestroyParticles(level: Level, player: Player, pos: BlockPos, state: BlockState) {
val blockEntity = level.getBlockEntity(pos) as? GrottoBlockEntity
val blockEntity = level.getBlockEntity(pos) as? HabitatBlockEntity
val mimicState = blockEntity?.mimickedState ?: return
mimicState.block.spawnDestroyParticles(level, player, pos, mimicState)
}
override fun onPlace(state: BlockState, level: Level, pos: BlockPos, oldState: BlockState, isMoving: Boolean) {
super.onPlace(state, level, pos, oldState, isMoving)
val blockEntity = level.getBlockEntity(pos) as? GrottoBlockEntity
val blockEntity = level.getBlockEntity(pos) as? HabitatBlockEntity
blockEntity?.updateLevelAndPos(level, pos)
}
override fun getSoundType(state: BlockState): SoundType {
val level = this.level ?: return super.getSoundType(state)
val pos = this.pos ?: return super.getSoundType(state)
val blockEntity = level.getBlockEntity(pos) as? GrottoBlockEntity
val blockEntity = level.getBlockEntity(pos) as? HabitatBlockEntity
val mimicState = blockEntity?.mimickedState ?: return super.getSoundType(state)
return mimicState.soundType
}
@@ -0,0 +1,335 @@
/*
* 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.block.habitat
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatPool
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatSpawn
import com.cobblemon.mod.common.api.habitats.HabitatPhaseOrder
import com.cobblemon.mod.common.api.habitats.NaturalHabitatPool
import com.cobblemon.mod.common.api.habitats.dto.HabitatSettingsDTO
import com.cobblemon.mod.common.api.habitats.spawningstyle.ActivatedHabitatSpawning
import com.cobblemon.mod.common.api.habitats.spawningstyle.HabitatSpawningStyle
import com.cobblemon.mod.common.api.habitats.spawningstyle.NaturalHabitatSpawning
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.net.messages.client.habitat.OpenHabitatBlockEditorPacket
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import kotlin.random.Random
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.StringTag
import net.minecraft.nbt.Tag
import net.minecraft.network.protocol.Packet
import net.minecraft.network.protocol.game.ClientGamePacketListener
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
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.phys.BlockHitResult
class HabitatBlockEntity(pos: BlockPos, state: BlockState) :
BlockEntity(CobblemonBlockEntities.HABITAT_BLOCK, pos, state),
SpawningInfluence {
var initialized = false
var receivingSignal = false
val spawnedEntityIDs = mutableSetOf<Int>()
var spawningStyle: HabitatSpawningStyle = ActivatedHabitatSpawning(this)
var numberOfPhases: Int = 1
var phaseOrder = HabitatPhaseOrder.SIMPLE
var currentPhase = 1
var levelRange: IntRange = 1..Cobblemon.config.maxPokemonLevel
var modifiers: PokemonProperties = PokemonProperties()
var mimicId: ResourceLocation = BuiltInRegistries.BLOCK.getKey(Blocks.STONE)
val mimickedState: BlockState
get() = BuiltInRegistries.BLOCK.get(mimicId).defaultBlockState() ?: Blocks.STONE.defaultBlockState()
var displaySpeciesIds: List<ResourceLocation> = emptyList()
fun onUse(player: Player, hit: BlockHitResult): InteractionResult {
if (player.level().isClientSide) {
return InteractionResult.SUCCESS
}
player as ServerPlayer
if (!player.isCreative) {
return InteractionResult.PASS
}
player.sendPacket(OpenHabitatBlockEditorPacket(this))
return InteractionResult.SUCCESS
}
/**
* Accepts that some changes have been made to the block settings and therefore we should re-initialize a bunch of stuff.
*/
fun refreshFromSettings() {
val spawningStyle = spawningStyle
var activatedStyle = false
var cancelsRegularSpawns = false
if (spawningStyle is ActivatedHabitatSpawning) {
spawningStyle.generateSpawnDetails(level as ServerLevel, blockPos)
activatedStyle = true
cancelsRegularSpawns = spawningStyle.cancelledNaturalSpawningRange > 0
} else if (spawningStyle is NaturalHabitatSpawning) {
spawningStyle.generateSpawnDetails()
cancelsRegularSpawns = spawningStyle.replaceSpawns
}
refreshDisplaySpeciesIds()
level!!.setBlockAndUpdate(
blockPos,
blockState
.setValue(HabitatBlock.ACTIVATED_STYLE, activatedStyle)
.setValue(HabitatBlock.CANCELS_REGULAR_SPAWNS, cancelsRegularSpawns)
)
}
fun calculatePhase(gameTime: Long): Int {
if (numberOfPhases <= 1) {
return 1
}
val basePhase = ((gameTime / 24000) % numberOfPhases).toInt() + 1
return when (phaseOrder) {
HabitatPhaseOrder.FULL_RANDOM -> {
// We want this to be deterministic based on the day, but also want it to be different for each block. So we use the block position as a salt.
Random(gameTime / 24000 + blockPos.asLong()).nextInt(numberOfPhases) + 1
}
HabitatPhaseOrder.FIXED_RANDOM -> calculatePhaseOrder()[basePhase - 1]
HabitatPhaseOrder.SIMPLE -> basePhase
}
}
/** Returns a list of all the possible phases in the order that is based on the block position of the habitat block. */
fun calculatePhaseOrder(): List<Int> {
return (1..numberOfPhases).shuffled(Random(blockPos.asLong()))
}
fun applySettings(habitatSettingsDTO: HabitatSettingsDTO) {
val activatedSettings = habitatSettingsDTO.activatedSettings
val activatedPool = habitatSettingsDTO.activatedSettings?.habitatPool?.toHabitatPool() as? ActivatedHabitatPool
val isActivated = habitatSettingsDTO.isActivatedSpawning
if (isActivated && activatedPool == null) {
return // Pool was bad, check console logs
}
val naturalSettings = habitatSettingsDTO.naturalSettings
val naturalPool = habitatSettingsDTO.naturalSettings?.habitatPool?.toHabitatPool() as? NaturalHabitatPool
if (!isActivated && naturalPool == null) {
return // Pool was bad, check console logs
}
this.numberOfPhases = habitatSettingsDTO.numberOfPhases
this.phaseOrder = habitatSettingsDTO.phaseOrder
this.levelRange = habitatSettingsDTO.levelRange
this.modifiers = PokemonProperties.parse(habitatSettingsDTO.modifiers)
this.mimicId = habitatSettingsDTO.mimicId
if (isActivated && activatedSettings != null) {
val activatedHabitatSpawning = ActivatedHabitatSpawning(this)
activatedHabitatSpawning.trigger = activatedSettings.trigger
activatedHabitatSpawning.chance = activatedSettings.chance
activatedHabitatSpawning.cancelledNaturalSpawningRange = activatedSettings.cancelledNaturalSpawningRange
activatedHabitatSpawning.spawnRange = activatedSettings.spawnRange
activatedHabitatSpawning.maxSpawns = activatedSettings.maxSpawns
activatedHabitatSpawning.maxSpawnsPerActivation = activatedSettings.maxSpawnsPerActivation
activatedHabitatSpawning.pool = activatedPool!!
this.spawningStyle = activatedHabitatSpawning
} else if (naturalSettings != null && naturalPool != null) {
val naturalHabitatSpawning = NaturalHabitatSpawning(this)
naturalHabitatSpawning.replaceSpawns = naturalSettings.replaceSpawns
naturalHabitatSpawning.rangeOfInfluence = naturalSettings.rangeOfInfluence
naturalHabitatSpawning.pool = naturalPool
this.spawningStyle = naturalHabitatSpawning
}
refreshFromSettings()
setChanged()
level?.sendBlockUpdated(worldPosition, blockState, blockState, Block.UPDATE_ALL)
}
override fun saveAdditional(tag: CompoundTag, registryLookup: HolderLookup.Provider) {
super.saveAdditional(tag, registryLookup)
tag.putString(DataKeys.HABITAT_MIMIC, mimicId.toString())
tag.putInt(DataKeys.HABITAT_PHASE_COUNT, numberOfPhases)
tag.putString(DataKeys.HABITAT_PHASE_ORDER, phaseOrder.toString())
// Only save the level range when it's a non-trivial one
if (levelRange.first != 1 || levelRange.last != Cobblemon.config.maxPokemonLevel) {
tag.putString(DataKeys.HABITAT_LEVEL_RANGE, IntRangeAdapter.serialize(levelRange))
}
tag.putString(DataKeys.HABITAT_MODIFIERS, modifiers.asString())
tag.putString(DataKeys.HABITAT_SPAWNING_STYLE, spawningStyle.type.toString())
spawningStyle.writeToNBT(tag)
val speciesListTag = ListTag()
displaySpeciesIds.forEach { speciesListTag.add(StringTag.valueOf(it.toString())) }
tag.put(DISPLAY_SPECIES_KEY, speciesListTag)
}
override fun loadAdditional(tag: CompoundTag, registryLookup: HolderLookup.Provider) {
super.loadAdditional(tag, registryLookup)
tag.getString(DataKeys.HABITAT_MIMIC).let {
mimicId = ResourceLocation.tryParse(it) ?: BuiltInRegistries.BLOCK.getKey(Blocks.STONE)
}
displaySpeciesIds = if (tag.contains(DISPLAY_SPECIES_KEY, Tag.TAG_LIST.toInt())) {
tag.getList(DISPLAY_SPECIES_KEY, Tag.TAG_STRING.toInt())
.mapNotNull { speciesTag -> ResourceLocation.tryParse(speciesTag.asString) }
.distinct()
} else {
emptyList()
}
if (level?.isClientSide == true) {
return // Client doesn't need to try loading the rest
}
numberOfPhases = tag.getInt(DataKeys.HABITAT_PHASE_COUNT)
phaseOrder = HabitatPhaseOrder.valueOf(tag.getString(DataKeys.HABITAT_PHASE_ORDER))
levelRange = tag.getString(DataKeys.HABITAT_LEVEL_RANGE).takeIf { it.isNotBlank() }?.let(IntRangeAdapter::deserialize) ?: 1..Cobblemon.config.maxPokemonLevel
modifiers = tag.getString(DataKeys.HABITAT_MODIFIERS).takeIf { it.isNotBlank() }?.let(PokemonProperties::parse) ?: PokemonProperties()
val spawningStyleType = tag.getString(DataKeys.HABITAT_SPAWNING_STYLE).asIdentifierDefaultingNamespace()
spawningStyle = if (spawningStyleType == ActivatedHabitatSpawn.TYPE) {
ActivatedHabitatSpawning(this)
} else {
NaturalHabitatSpawning(this)
}
spawningStyle.readFromNBT(tag)
refreshDisplaySpeciesIds()
}
override fun getUpdatePacket(): Packet<ClientGamePacketListener> {
return ClientboundBlockEntityDataPacket.create(this)
}
override fun getUpdateTag(registryLookup: HolderLookup.Provider): CompoundTag {
// Don't tell the client everything, otherwise hackermans may learn too much
val tag = CompoundTag()
tag.putString(DataKeys.HABITAT_MIMIC, mimicId.toString())
val speciesListTag = ListTag()
displaySpeciesIds.forEach { speciesListTag.add(StringTag.valueOf(it.toString())) }
tag.put(DISPLAY_SPECIES_KEY, speciesListTag)
return tag
}
private fun refreshDisplaySpeciesIds() {
displaySpeciesIds = when (val style = spawningStyle) {
is ActivatedHabitatSpawning -> style.pool.spawns.map { it.species.resourceIdentifier }.distinct()
is NaturalHabitatSpawning -> style.pool.spawns.map { it.species.resourceIdentifier }.distinct()
}
}
fun getInfluentialRange(spawner: Spawner): Int {
val spawningStyle = spawningStyle
return if (spawningStyle is ActivatedHabitatSpawning) {
spawningStyle.cancelledNaturalSpawningRange
} else if (spawningStyle is NaturalHabitatSpawning) {
if (spawner == Cobblemon.bestSpawner.fishingSpawner && !spawningStyle.affectsFishing) {
0 // We don't impact fishing
} else if (spawner != Cobblemon.bestSpawner.fishingSpawner && !spawningStyle.affectsOverworld) {
0 // We don't impact non-fishing spawns. I'd like it if we could tag spawners better than what I'm doing here.
} else {
spawningStyle.rangeOfInfluence
}
} else {
0
}
}
fun updateLevelAndPos(level: Level, pos: BlockPos) {
val block = this.blockState.block
if (block is HabitatBlock) {
block.level = level
block.pos = pos
}
}
override fun affectSpawnable(detail: SpawnDetail, spawnablePosition: SpawnablePosition): Boolean {
val spawningStyle = spawningStyle
return if (spawningStyle is NaturalHabitatSpawning) {
if (spawningStyle.replaceSpawns) {
// Replace mode! Permit spawning only if it's from this block
detail in spawningStyle.spawnDetails
} else {
true // We don't mess with other types
}
} else if (spawningStyle is ActivatedHabitatSpawning) {
// If this influence is on your spawnable position then it's because we have cancellation on or it's our own spawner, only allow OUR spawns
spawningStyle.spawner == spawnablePosition.spawner
} else {
true // Not likely
}
}
override fun injectSpawns(bucket: SpawnBucket, spawnablePosition: SpawnablePosition): List<SpawnDetail>? {
val spawningStyle = spawningStyle
return if (spawningStyle is NaturalHabitatSpawning) {
spawningStyle.spawnDetails.filter { it.spawnablePositionType.clazz == spawnablePosition::class.java && it.isSatisfiedBy(spawnablePosition) }
} else {
null
}
}
companion object {
private const val DISPLAY_SPECIES_KEY = "DisplaySpecies"
val TICKER = BlockEntityTicker<HabitatBlockEntity> { world, pos, _, blockEntity ->
if (world is ServerLevel) {
if (!blockEntity.initialized) {
blockEntity.refreshFromSettings()
blockEntity.initialized = true
}
val style = blockEntity.spawningStyle
// Every couple of seconds, check what phase we should be in
if (world.gameTime % 40 == 0L) {
val desiredPhase = blockEntity.calculatePhase(world.gameTime)
if (blockEntity.currentPhase != desiredPhase) {
blockEntity.currentPhase = desiredPhase
blockEntity.setChanged()
}
}
if (style is ActivatedHabitatSpawning) {
if (world.gameTime % 20 == 0L) {
blockEntity.spawnedEntityIDs.removeIf { id -> world.getEntity(id)?.isAlive != true }
}
if (style.trigger == ActivatedHabitatSpawning.Trigger.TICK) {
style.activate(world, pos) // TODO if it spawns something it would be good to emit a redstone pulse
} else if (style.trigger == ActivatedHabitatSpawning.Trigger.REDSTONE) {
val hasSignalNow = world.getDirectSignalTo(pos) > 0
if (blockEntity.receivingSignal && !hasSignalNow) {
blockEntity.receivingSignal = false
} else if (!blockEntity.receivingSignal && hasSignalNow) {
blockEntity.receivingSignal = true
style.activate(world, pos)
}
}
}
}
}
}
}
@@ -8,7 +8,6 @@
package com.cobblemon.mod.common.client
import com.cobblemon.mod.client.render.blockentity.GrottoBlockRenderer
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonBlocks
@@ -34,16 +33,7 @@ import com.cobblemon.mod.common.client.gui.tmmachine.TMMachineScreen
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.render.DeferredRenderer
import com.cobblemon.mod.common.client.render.block.BerryBlockRenderer
import com.cobblemon.mod.common.client.render.block.CampfireBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.DisplayCaseRenderer
import com.cobblemon.mod.common.client.render.block.FossilAnalyzerRenderer
import com.cobblemon.mod.common.client.render.block.GildedChestBlockRenderer
import com.cobblemon.mod.common.client.render.block.HealingMachineRenderer
import com.cobblemon.mod.common.client.render.block.LecternBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.PokeSnackBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.RestorationTankRenderer
import com.cobblemon.mod.common.client.render.block.TMShelfBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.*
import com.cobblemon.mod.common.client.render.boat.CobblemonBoatRenderer
import com.cobblemon.mod.common.client.render.color.AprijuiceItemColorProvider
import com.cobblemon.mod.common.client.render.color.PokeBaitItemColorProvider
@@ -441,7 +431,7 @@ object CobblemonClient {
CobblemonBlockEntities.RESTORATION_TANK,
::RestorationTankRenderer
)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.GROTTO_BLOCK, ::GrottoBlockRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.HABITAT_BLOCK, ::HabitatBlockRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.GILDED_CHEST, ::GildedChestBlockRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.DISPLAY_CASE, ::DisplayCaseRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.LECTERN, ::LecternBlockEntityRenderer)
@@ -1,62 +0,0 @@
package com.cobblemon.mod.common.client.gui.debug
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.block.entity.GrottoBlockEntity
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.net.messages.server.debug.UpdateGrottoBlockPacket
import com.cobblemon.mod.common.util.lang
import java.awt.Color
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.screens.Screen
class GrottoDebugGUI(val grottoEntity: GrottoBlockEntity) : Screen(lang("ui.debug.grotto")), CobblemonRenderable {
private lateinit var mimicCycleButton: Button
override fun init() {
super.init()
mimicCycleButton = addRenderableWidget(
Button.builder("Block Mimicking: ${grottoEntity.mimicId.namespace}:${grottoEntity.mimicId.path}".text()) { button ->
val currentIndex = grottoEntity.mimicCycle.indexOf(grottoEntity.mimicId)
val nextIndex = (currentIndex + 1) % grottoEntity.mimicCycle.size
grottoEntity.mimicId = grottoEntity.mimicCycle[nextIndex]
refresh()
}.bounds(10, 10, getScaledWidth() - 20, 20).build()
)
addRenderableWidget(
Button.builder("Save".text()) { button ->
saveEverything()
}.bounds(10, getScaledHeight() - 20, getScaledWidth() - 20, 20).build()
)
refresh()
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, width, height, Color(0, 0, 0, 100).rgb)
super.render(guiGraphics, mouseX, mouseY, partialTick)
}
fun refresh() {
mimicCycleButton.message =
"Block Mimicking: ${grottoEntity.mimicId.namespace}:${grottoEntity.mimicId.path}".text()
}
fun saveEverything() {
CobblemonNetwork.sendToServer(
UpdateGrottoBlockPacket(
grottoEntity.blockPos
)
)
refresh()
Minecraft.getInstance().setScreen(null)
}
fun getScaledWidth() = Minecraft.getInstance().window.guiScaledWidth
fun getScaledHeight() = Minecraft.getInstance().window.guiScaledHeight
}
@@ -0,0 +1,576 @@
/*
* 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.habitat
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatPool
import com.cobblemon.mod.common.api.habitats.HabitatPhaseOrder
import com.cobblemon.mod.common.api.habitats.NaturalHabitatPool
import com.cobblemon.mod.common.api.habitats.dto.ActivatedHabitatSettingsDTO
import com.cobblemon.mod.common.api.habitats.dto.HabitatPoolSettingsDTO
import com.cobblemon.mod.common.api.habitats.dto.HabitatSettingsDTO
import com.cobblemon.mod.common.api.habitats.dto.NaturalHabitatSettingsDTO
import com.cobblemon.mod.common.api.habitats.spawningstyle.ActivatedHabitatSpawning
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.tags.CobblemonBlockTags
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.net.messages.server.habitat.SaveHabitatBlockSettingsPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import java.awt.Color
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.screens.Screen
import net.minecraft.core.BlockPos
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
class HabitatEditGUI(
/** The position of the habitat block in the client player's world */
val blockPos: BlockPos,
/** How many Pokémon are alive nearby as a result of this block, fyi */
val currentlySpawned: Int,
/** The current phase of the habitat block, fyi */
val currentPhase: Int,
/** The highest level possible for a Pokémon according to the server's config */
val maxPokemonLevel: Int,
/** Use this as reference data for what spawnable position types exist. */
val spawnablePositionTypes: List<String>,
/** Use this as reference data for what buckets can be chosen and how rare they are */
val buckets: List<SpawnBucket>,
/** Use this as reference data for pre-made activated habitat pools */
val activatedHabitatPools: Map<ResourceLocation, ActivatedHabitatPool>,
/** Use this as reference data for pre-made natural habitat pools */
val naturalHabitatPools: Map<ResourceLocation, NaturalHabitatPool>,
/** The current settings - edit the properties in here then send it back to the server. */
val habitatSettingsDTO: HabitatSettingsDTO
) : Screen(lang("ui.edit.habitat")), CobblemonRenderable {
private var scrollOffset = 0.0
private var minScrollOffset = 0.0
private val headerHeight = 24
private val footerHeight = 44
private val optionsTopPadding = 6
private val saveButtonHeight = 20
private val saveButtonBottomMargin = 10
private val optionsBottomPadding = 6
private val validTextColor = 0xFFFFFF
private val invalidTextColor = 0xFF5555
private lateinit var mimicInput: EditBox
private lateinit var phaseOrderButton: Button
private lateinit var spawnModeButton: Button
private lateinit var poolSelectorButton: Button
private lateinit var viewPoolButton: Button
private lateinit var levelMinInput: EditBox
private lateinit var levelMaxInput: EditBox
private lateinit var saveButton: Button
private val optionWidgets = mutableListOf<AbstractWidget>()
private val footerWidgets = mutableListOf<AbstractWidget>()
private val validationErrors = linkedMapOf<String, String>()
private var validationMessageY = 0
private val labels = mutableListOf<Pair<String, Pair<Int, Int>>>()
override fun init() {
super.init()
validationErrors.clear()
optionWidgets.clear()
footerWidgets.clear()
scrollOffset = scrollOffset.coerceIn(minScrollOffset, 0.0)
labels.clear()
var y = getOptionsTop() + scrollOffset.toInt()
val spacing = 25
val buttonWidth = getScaledWidth() - 20
// Common Settings
labels.add("Mimic Block ID" to (10 to y))
y += 12
mimicInput = EditBox(font, 10, y, buttonWidth, 20, "Mimic Block".text())
mimicInput.value = habitatSettingsDTO.mimicId.toString()
mimicInput.setResponder { value ->
val id = ResourceLocation.tryParse(value)
val valid = id != null &&
BuiltInRegistries.BLOCK.containsKey(id) &&
BuiltInRegistries.BLOCK.get(id).builtInRegistryHolder().`is`(CobblemonBlockTags.HABITAT_MIMICS)
if (valid) {
habitatSettingsDTO.mimicId = id!!
}
mimicInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("mimic", valid, "ui.edit.habitat.validation.mimic")
}
val initialMimicId = ResourceLocation.tryParse(mimicInput.value)
val initialMimicValid = initialMimicId != null &&
BuiltInRegistries.BLOCK.containsKey(initialMimicId) &&
BuiltInRegistries.BLOCK.get(initialMimicId).builtInRegistryHolder().`is`(CobblemonBlockTags.HABITAT_MIMICS)
mimicInput.setTextColor(if (initialMimicValid) validTextColor else invalidTextColor)
setFieldValidity("mimic", initialMimicValid, "ui.edit.habitat.validation.mimic")
addOptionWidget(mimicInput)
y += spacing
labels.add("Number of Phases" to (10 to y))
y += 12
val phasesInput = EditBox(font, 10, y, buttonWidth, 20, "Phases".text())
phasesInput.value = habitatSettingsDTO.numberOfPhases.toString()
phasesInput.setResponder { value ->
val phases = value.toIntOrNull()
val valid = phases != null && phases > 0
if (valid) {
habitatSettingsDTO.numberOfPhases = phases!!
}
phasesInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("phases", valid, "ui.edit.habitat.validation.phases")
}
val initialPhases = phasesInput.value.toIntOrNull()
val initialPhasesValid = initialPhases != null && initialPhases > 0
phasesInput.setTextColor(if (initialPhasesValid) validTextColor else invalidTextColor)
setFieldValidity("phases", initialPhasesValid, "ui.edit.habitat.validation.phases")
addOptionWidget(phasesInput)
y += spacing
phaseOrderButton = addOptionWidget(
Button.builder(getPhaseOrderText()) {
val orders = HabitatPhaseOrder.entries
val currentIndex = orders.indexOf(habitatSettingsDTO.phaseOrder)
habitatSettingsDTO.phaseOrder = orders[(currentIndex + 1) % orders.size]
refresh()
}.bounds(10, y, buttonWidth, 20).build()
)
y += spacing
labels.add("Level Range (Min - Max)" to (10 to y))
y += 12
levelMinInput = EditBox(font, 10, y, buttonWidth / 2 - 5, 20, "Min Level".text())
levelMinInput.value = habitatSettingsDTO.levelRange.first.toString()
levelMinInput.setResponder { value ->
validateLevelRangeInputs()
}
addOptionWidget(levelMinInput)
levelMaxInput = EditBox(font, 10 + buttonWidth / 2 + 5, y, buttonWidth / 2 - 5, 20, "Max Level".text())
levelMaxInput.value = habitatSettingsDTO.levelRange.last.toString()
levelMaxInput.setResponder { value ->
validateLevelRangeInputs()
}
addOptionWidget(levelMaxInput)
validateLevelRangeInputs()
y += spacing
// Spawning Mode Toggle
spawnModeButton = addOptionWidget(
Button.builder(getSpawnModeText()) {
habitatSettingsDTO.isActivatedSpawning = !habitatSettingsDTO.isActivatedSpawning
if (habitatSettingsDTO.isActivatedSpawning && habitatSettingsDTO.activatedSettings == null) {
habitatSettingsDTO.activatedSettings = ActivatedHabitatSettingsDTO()
}
if (!habitatSettingsDTO.isActivatedSpawning && habitatSettingsDTO.naturalSettings == null) {
habitatSettingsDTO.naturalSettings = NaturalHabitatSettingsDTO()
}
rebuildWidgets()
}.bounds(10, y, buttonWidth, 20).build()
)
y += spacing
// Mode-Specific Settings
if (habitatSettingsDTO.isActivatedSpawning) {
val activated = habitatSettingsDTO.activatedSettings!!
addOptionWidget(
Button.builder(getTriggerText()) {
val triggers = ActivatedHabitatSpawning.Trigger.entries
val currentIndex = triggers.indexOf(activated.trigger)
activated.trigger = triggers[(currentIndex + 1) % triggers.size]
it.message = getTriggerText()
refresh()
}.bounds(10, y, buttonWidth, 20).build()
)
y += spacing
labels.add("Spawn Range (blocks)" to (10 to y))
y += 12
val spawnRangeInput = EditBox(font, 10, y, buttonWidth, 20, "Spawn Range".text())
spawnRangeInput.value = activated.spawnRange.toString()
spawnRangeInput.setResponder { value ->
val parsed = value.toIntOrNull()
val valid = parsed != null && parsed >= 0
if (valid) {
activated.spawnRange = parsed!!
}
spawnRangeInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("spawn_range", valid, "ui.edit.habitat.validation.spawn_range")
}
val initialSpawnRange = spawnRangeInput.value.toIntOrNull()
val initialSpawnRangeValid = initialSpawnRange != null && initialSpawnRange >= 0
spawnRangeInput.setTextColor(if (initialSpawnRangeValid) validTextColor else invalidTextColor)
setFieldValidity("spawn_range", initialSpawnRangeValid, "ui.edit.habitat.validation.spawn_range")
addOptionWidget(spawnRangeInput)
y += spacing
labels.add("Cancelled Natural Spawning Range (blocks)" to (10 to y))
y += 12
val cancelledRangeInput = EditBox(font, 10, y, buttonWidth, 20, "Cancelled Range".text())
cancelledRangeInput.value = activated.cancelledNaturalSpawningRange.toString()
cancelledRangeInput.setResponder { value ->
val parsed = value.toIntOrNull()
val valid = parsed != null && (parsed == -1 || parsed in 0..64)
if (valid) {
activated.cancelledNaturalSpawningRange = parsed!!
}
cancelledRangeInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("cancelled_range", valid, "ui.edit.habitat.validation.cancelled_range")
}
val initialCancelledRange = cancelledRangeInput.value.toIntOrNull()
val initialCancelledRangeValid = initialCancelledRange != null && (initialCancelledRange == -1 || initialCancelledRange in 0..64)
cancelledRangeInput.setTextColor(if (initialCancelledRangeValid) validTextColor else invalidTextColor)
setFieldValidity("cancelled_range", initialCancelledRangeValid, "ui.edit.habitat.validation.cancelled_range")
addOptionWidget(cancelledRangeInput)
y += spacing
labels.add("Max Spawns (-1 for infinite)" to (10 to y))
y += 12
val maxSpawnsInput = EditBox(font, 10, y, buttonWidth, 20, "Max Spawns".text())
maxSpawnsInput.value = activated.maxSpawns.toString()
maxSpawnsInput.setResponder { value ->
val parsed = value.toIntOrNull()
val valid = parsed != null && parsed >= -1
if (valid) {
activated.maxSpawns = parsed!!
}
maxSpawnsInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("max_spawns", valid, "ui.edit.habitat.validation.max_spawns")
}
val initialMaxSpawns = maxSpawnsInput.value.toIntOrNull()
val initialMaxSpawnsValid = initialMaxSpawns != null && initialMaxSpawns >= -1
maxSpawnsInput.setTextColor(if (initialMaxSpawnsValid) validTextColor else invalidTextColor)
setFieldValidity("max_spawns", initialMaxSpawnsValid, "ui.edit.habitat.validation.max_spawns")
addOptionWidget(maxSpawnsInput)
y += spacing
} else {
val natural = habitatSettingsDTO.naturalSettings!!
addOptionWidget(
Button.builder(getReplaceSpawnsText()) {
natural.replaceSpawns = !natural.replaceSpawns
it.message = getReplaceSpawnsText()
refresh()
}.bounds(10, y, buttonWidth, 20).build()
)
y += spacing
labels.add("Range of Influence (blocks)" to (10 to y))
y += 12
val rangeInput = EditBox(font, 10, y, buttonWidth, 20, "Range of Influence".text())
rangeInput.value = natural.rangeOfInfluence.toString()
rangeInput.setResponder { value ->
val parsed = value.toIntOrNull()
val valid = parsed != null && parsed >= 0
if (valid) {
natural.rangeOfInfluence = parsed!!
}
rangeInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("influence_range", valid, "ui.edit.habitat.validation.influence_range")
}
val initialInfluenceRange = rangeInput.value.toIntOrNull()
val initialInfluenceRangeValid = initialInfluenceRange != null && initialInfluenceRange >= 0
rangeInput.setTextColor(if (initialInfluenceRangeValid) validTextColor else invalidTextColor)
setFieldValidity("influence_range", initialInfluenceRangeValid, "ui.edit.habitat.validation.influence_range")
addOptionWidget(rangeInput)
y += spacing
}
// Pool Selection
poolSelectorButton = addOptionWidget(
Button.builder(getPoolText()) {
val currentPool = if (habitatSettingsDTO.isActivatedSpawning) {
habitatSettingsDTO.activatedSettings?.habitatPool
} else {
habitatSettingsDTO.naturalSettings?.habitatPool
}
val selectedPoolId = if (currentPool?.isReference == true) currentPool.id else null
Minecraft.getInstance().setScreen(
HabitatPoolSelectionGUI(
isActivated = habitatSettingsDTO.isActivatedSpawning,
activatedPools = activatedHabitatPools,
naturalPools = naturalHabitatPools,
selectedPoolId = selectedPoolId,
parentScreen = this
) { poolId ->
applyPoolSelection(poolId)
}
)
}.bounds(10, y, buttonWidth, 20).build()
)
y += spacing
viewPoolButton = addOptionWidget(
Button.builder(getViewPoolText()) {
val pool = if (habitatSettingsDTO.isActivatedSpawning) {
habitatSettingsDTO.activatedSettings?.habitatPool
} else {
habitatSettingsDTO.naturalSettings?.habitatPool
} ?: return@builder
if (!pool.isReference) {
Minecraft.getInstance().setScreen(
HabitatPoolEditorGUI(pool, habitatSettingsDTO.isActivatedSpawning, buckets, spawnablePositionTypes, maxPokemonLevel, this, editable = true)
)
return@builder
}
val sourcePool = if (habitatSettingsDTO.isActivatedSpawning) {
activatedHabitatPools[pool.id]
} else {
naturalHabitatPools[pool.id]
} ?: return@builder
val viewPool = HabitatPoolSettingsDTO(sourcePool).apply {
isReference = true
id = sourcePool.id
isActivated = habitatSettingsDTO.isActivatedSpawning
name = sourcePool.name
spawns = sourcePool.spawns.toMutableList()
}
Minecraft.getInstance().setScreen(
HabitatPoolEditorGUI(viewPool, habitatSettingsDTO.isActivatedSpawning, buckets, spawnablePositionTypes, maxPokemonLevel, this, editable = false)
)
}.bounds(10, y, buttonWidth, 20).build()
)
y += spacing
val contentBottomAtZeroOffset = y - scrollOffset.toInt()
updateScrollBounds(contentBottomAtZeroOffset)
if (scrollOffset < minScrollOffset) {
scrollOffset = minScrollOffset
rebuildWidgets()
return
}
// Save Button (fixed at bottom)
saveButton = addFooterWidget(
Button.builder("Save".text()) { _ -> save() }
.bounds(10, getScaledHeight() - (saveButtonHeight + saveButtonBottomMargin), buttonWidth, saveButtonHeight)
.build()
)
validationMessageY = getFooterTop() + 6
updateSaveButtonState()
updateOptionWidgetInteractivity()
refresh()
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
if (mouseY < getOptionsTop() || mouseY > getOptionsBottom()) {
return false
}
scrollOffset = (scrollOffset + scrollY * 10).coerceIn(minScrollOffset, 0.0)
rebuildWidgets()
return true
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, width, height, Color(0, 0, 0, 100).rgb)
guiGraphics.fill(0, 0, width, headerHeight, Color(0, 0, 0, 150).rgb)
guiGraphics.fill(0, getFooterTop(), width, height, Color(0, 0, 0, 150).rgb)
guiGraphics.drawCenteredString(font, title, width / 2, 8, 0xFFFFFF)
guiGraphics.enableScissor(0, getOptionsTop(), width, getOptionsBottom())
optionWidgets.forEach { widget ->
widget.render(guiGraphics, mouseX, mouseY, partialTick)
}
val optionsBottom = getOptionsBottom()
labels.forEach { (label, pos) ->
if (pos.second >= getOptionsTop() && pos.second + font.lineHeight <= optionsBottom) {
guiGraphics.drawString(font, label, pos.first, pos.second, 0xFFFFFF)
}
}
guiGraphics.disableScissor()
footerWidgets.forEach { widget ->
widget.render(guiGraphics, mouseX, mouseY, partialTick)
}
validationErrors.values.firstOrNull()?.let { errorKey ->
guiGraphics.drawString(font, lang(errorKey), 10, validationMessageY, invalidTextColor)
}
}
fun refresh() {
phaseOrderButton.message = getPhaseOrderText()
spawnModeButton.message = getSpawnModeText()
poolSelectorButton.message = getPoolText()
viewPoolButton.message = getViewPoolText()
}
private fun getPhaseOrderText() = "Phase Order: ${habitatSettingsDTO.phaseOrder}".text()
private fun getSpawnModeText() = "Mode: ${if (habitatSettingsDTO.isActivatedSpawning) "Activated" else "Natural"}".text()
private fun getTriggerText() = "Trigger: ${habitatSettingsDTO.activatedSettings?.trigger}".text()
private fun getReplaceSpawnsText() = "Replace Spawns: ${habitatSettingsDTO.naturalSettings?.replaceSpawns ?: false}".text()
private fun getPoolText(): net.minecraft.network.chat.Component {
val pool = if (habitatSettingsDTO.isActivatedSpawning) {
habitatSettingsDTO.activatedSettings?.habitatPool
} else {
habitatSettingsDTO.naturalSettings?.habitatPool
}
if (pool == null || !pool.isReference) {
return "Pool: Custom".text()
}
val poolName = if (habitatSettingsDTO.isActivatedSpawning) {
activatedHabitatPools[pool.id]?.name
} else {
naturalHabitatPools[pool.id]?.name
}
return "Pool: ${poolName ?: pool.id}".text()
}
private fun getViewPoolText(): net.minecraft.network.chat.Component {
val pool = if (habitatSettingsDTO.isActivatedSpawning) {
habitatSettingsDTO.activatedSettings?.habitatPool
} else {
habitatSettingsDTO.naturalSettings?.habitatPool
}
return if (pool?.isReference == true) {
"View Pool".text()
} else {
"Edit Spawn Pool".text()
}
}
private fun applyPoolSelection(poolId: ResourceLocation?) {
val currentPool = if (habitatSettingsDTO.isActivatedSpawning) {
habitatSettingsDTO.activatedSettings?.habitatPool
} else {
habitatSettingsDTO.naturalSettings?.habitatPool
}
if (poolId == null) {
if (currentPool == null || currentPool.isReference) {
val newCustomPool = HabitatPoolSettingsDTO().apply {
id = cobblemonResource("custom__${System.currentTimeMillis()}")
isReference = false
isActivated = habitatSettingsDTO.isActivatedSpawning
name = "Custom"
spawns = mutableListOf()
}
if (habitatSettingsDTO.isActivatedSpawning) {
habitatSettingsDTO.activatedSettings?.habitatPool = newCustomPool
} else {
habitatSettingsDTO.naturalSettings?.habitatPool = newCustomPool
}
}
return
}
val sourcePool = if (habitatSettingsDTO.isActivatedSpawning) {
activatedHabitatPools[poolId]
} else {
naturalHabitatPools[poolId]
} ?: return
val selectedPool = HabitatPoolSettingsDTO(sourcePool).apply {
isReference = true
id = poolId
isActivated = habitatSettingsDTO.isActivatedSpawning
name = sourcePool.name
}
if (habitatSettingsDTO.isActivatedSpawning) {
habitatSettingsDTO.activatedSettings?.habitatPool = selectedPool
} else {
habitatSettingsDTO.naturalSettings?.habitatPool = selectedPool
}
}
private fun validateLevelRangeInputs() {
val min = levelMinInput.value.toIntOrNull()
val max = levelMaxInput.value.toIntOrNull()
val minInRange = min != null && min in 1..maxPokemonLevel
val maxInRange = max != null && max in 1..maxPokemonLevel
val ordered = min != null && max != null && min <= max
val bothValid = minInRange && maxInRange && ordered
val orderInvalid = minInRange && maxInRange && !ordered
levelMinInput.setTextColor(if (minInRange && !orderInvalid) validTextColor else invalidTextColor)
levelMaxInput.setTextColor(if (maxInRange && !orderInvalid) validTextColor else invalidTextColor)
if (bothValid) {
habitatSettingsDTO.levelRange = min!!..max!!
}
setFieldValidity("level_range", bothValid, "ui.edit.habitat.validation.level_range")
}
private fun setFieldValidity(field: String, valid: Boolean, errorKey: String) {
if (valid) {
validationErrors.remove(field)
} else {
validationErrors[field] = errorKey
}
updateSaveButtonState()
}
private fun updateSaveButtonState() {
if (::saveButton.isInitialized) {
saveButton.active = validationErrors.isEmpty()
}
}
private fun updateOptionWidgetInteractivity() {
val optionsTop = getOptionsTop()
val optionsBottom = getOptionsBottom()
optionWidgets.forEach { widget ->
val widgetTop = widget.y
val widgetBottom = widget.y + widget.height
val inViewport = widgetBottom > optionsTop && widgetTop < optionsBottom
widget.visible = inViewport
widget.active = inViewport
if (!inViewport) {
widget.setFocused(false)
}
}
}
fun save() {
SaveHabitatBlockSettingsPacket(
blockPos = blockPos,
settings = habitatSettingsDTO
).sendToServer()
Minecraft.getInstance().setScreen(null)
}
private fun updateScrollBounds(contentBottomAtZeroOffset: Int) {
val optionsBottom = getOptionsBottom()
minScrollOffset = minOf(0.0, (optionsBottom - contentBottomAtZeroOffset).toDouble())
}
private fun getOptionsTop(): Int {
return headerHeight + optionsTopPadding
}
private fun getFooterTop(): Int {
return getScaledHeight() - footerHeight
}
private fun getOptionsBottom(): Int {
return getFooterTop() - optionsBottomPadding
}
private fun <T : AbstractWidget> addOptionWidget(widget: T): T {
optionWidgets.add(widget)
return addRenderableWidget(widget)
}
private fun <T : AbstractWidget> addFooterWidget(widget: T): T {
footerWidgets.add(widget)
return addRenderableWidget(widget)
}
fun getScaledWidth() = Minecraft.getInstance().window.guiScaledWidth
fun getScaledHeight() = Minecraft.getInstance().window.guiScaledHeight
}
@@ -0,0 +1,268 @@
/*
* 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.habitat
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatSpawn
import com.cobblemon.mod.common.api.habitats.NaturalHabitatSpawn
import com.cobblemon.mod.common.api.habitats.dto.HabitatPoolSettingsDTO
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.util.adapters.IntRangesAdapter
import com.cobblemon.mod.common.util.lang
import java.awt.Color
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.screens.Screen
class HabitatPoolEditorGUI(
val pool: HabitatPoolSettingsDTO,
val isActivated: Boolean,
val buckets: List<SpawnBucket>,
val spawnablePositionTypes: List<String>,
val maxPokemonLevel: Int,
val parentScreen: Screen,
val editable: Boolean = true
) : Screen(lang("ui.edit.habitat.pool")), CobblemonRenderable {
private var scrollOffset = 0.0
private var minScrollOffset = 0.0
private val headerHeight = 24
private val footerHeight = 44
private val optionsTopPadding = 6
private val optionsBottomPadding = 6
private val actionButtonHeight = 20
private val actionButtonBottomMargin = 10
private val optionWidgets = mutableListOf<AbstractWidget>()
private val footerWidgets = mutableListOf<AbstractWidget>()
private val headers = mutableListOf<Pair<String, Int>>()
private val spawnRows = mutableListOf<SpawnRowData>()
data class SpawnRowData(
val species: String,
val levels: String,
val phases: String,
val position: String,
val weight: String,
val bucket: String?,
val y: Int
)
override fun init() {
super.init()
optionWidgets.clear()
footerWidgets.clear()
headers.clear()
spawnRows.clear()
scrollOffset = scrollOffset.coerceIn(minScrollOffset, 0.0)
var y = getOptionsTop() + scrollOffset.toInt()
val spacing = 25
val buttonWidth = getScaledWidth() - 20
if (pool.spawns == null) {
pool.spawns = mutableListOf()
}
// Headers
val colWidth = if (isActivated) buttonWidth / 5 else buttonWidth / 6
var x = 10
headers.add("Species" to x)
x += colWidth
headers.add("Levels" to x)
x += colWidth
headers.add("Phases" to x)
x += colWidth
headers.add("Position" to x)
x += colWidth
headers.add("Weight" to x)
if (!isActivated) {
x += colWidth - 10
headers.add("Bucket" to x)
}
y += 20
var spawns = pool.spawns!!
if (!isActivated) {
spawns = spawns.sortedBy { (it as? NaturalHabitatSpawn)?.bucket?.name?.first() }.toMutableList()
}
spawns.forEachIndexed { index, spawn ->
val rowY = y
var levelMax = spawn.levelRange.last
if (levelMax == Int.MAX_VALUE) {
levelMax = maxPokemonLevel
}
spawnRows.add(SpawnRowData(
species = spawn.species.name,
levels = "${spawn.levelRange.first}-$levelMax",
phases = spawn.phases?.let { IntRangesAdapter.basic.serialize(it) } ?: "All",
position = spawn.spawnablePositionType,
weight = spawn.weight.toString(),
bucket = if (!isActivated && spawn is NaturalHabitatSpawn) spawn.bucket.name.first().uppercaseChar().toString() else null,
y = rowY
))
if (editable) {
addOptionWidget(
Button.builder("E".text()) {
Minecraft.getInstance().setScreen(HabitatSpawnEditorGUI(spawn, isActivated, maxPokemonLevel, buckets, spawnablePositionTypes, this))
}.bounds(getScaledWidth() - 55, rowY, 20, 20).build()
)
addOptionWidget(
Button.builder("X".text()) {
pool.spawns!!.removeAt(index)
rebuildWidgets()
}.bounds(getScaledWidth() - 30, rowY, 20, 20).build()
)
}
y += spacing
}
if (editable) {
addOptionWidget(
Button.builder("Add Spawn".text()) {
val newSpawn = if (isActivated) ActivatedHabitatSpawn() else NaturalHabitatSpawn()
newSpawn.species = PokemonSpecies.implemented.random()
newSpawn.spawnablePositionType = spawnablePositionTypes.firstOrNull() ?: "grounded"
if (newSpawn is NaturalHabitatSpawn) {
newSpawn.bucket = buckets.firstOrNull() ?: return@builder
}
pool.spawns!!.add(newSpawn)
rebuildWidgets()
}.bounds(10, y, buttonWidth, 20).build()
)
y += spacing
}
val contentBottomAtZeroOffset = y - scrollOffset.toInt()
updateScrollBounds(contentBottomAtZeroOffset)
if (scrollOffset < minScrollOffset) {
scrollOffset = minScrollOffset
rebuildWidgets()
return
}
addFooterWidget(
Button.builder("Back".text()) {
Minecraft.getInstance().setScreen(parentScreen)
}.bounds(10, getScaledHeight() - (actionButtonHeight + actionButtonBottomMargin), buttonWidth, actionButtonHeight).build()
)
updateOptionWidgetInteractivity()
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
if (mouseY < getOptionsTop() || mouseY > getOptionsBottom()) {
return false
}
scrollOffset = (scrollOffset + scrollY * 10).coerceIn(minScrollOffset, 0.0)
rebuildWidgets()
return true
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, width, height, Color(0, 0, 0, 100).rgb)
guiGraphics.fill(0, 0, width, headerHeight, Color(0, 0, 0, 150).rgb)
guiGraphics.fill(0, getFooterTop(), width, height, Color(0, 0, 0, 150).rgb)
guiGraphics.drawCenteredString(font, title, width / 2, 8, 0xFFFFFF)
val buttonWidth = getScaledWidth() - 20
val colWidth = if (isActivated) buttonWidth / 5 else buttonWidth / 6
val optionsTop = getOptionsTop()
val optionsBottom = getOptionsBottom()
guiGraphics.enableScissor(0, optionsTop, width, optionsBottom)
// Draw headers
headers.forEach { (header, x) ->
val y = optionsTop + 5 + scrollOffset.toInt()
if (y >= optionsTop && y + font.lineHeight <= optionsBottom) {
guiGraphics.drawString(font, header, x, y, 0xFFFFFF)
}
}
// Draw spawn rows
spawnRows.forEach { row ->
if (row.y < optionsTop || row.y + 20 > optionsBottom) return@forEach
var x = 10
guiGraphics.drawString(font, row.species, x, row.y + 5, 0xFFFFFF)
x += colWidth
guiGraphics.drawString(font, row.levels, x, row.y + 5, 0xFFFFFF)
x += colWidth
guiGraphics.drawString(font, row.phases, x, row.y + 5, 0xFFFFFF)
x += colWidth
guiGraphics.drawString(font, row.position, x, row.y + 5, 0xFFFFFF)
x += colWidth
guiGraphics.drawString(font, row.weight, x, row.y + 5, 0xFFFFFF)
if (row.bucket != null) {
x += colWidth - 10
guiGraphics.drawString(font, row.bucket, x, row.y + 5, 0xFFFFFF)
}
}
optionWidgets.forEach { widget ->
widget.render(guiGraphics, mouseX, mouseY, partialTick)
}
guiGraphics.disableScissor()
footerWidgets.forEach { widget ->
widget.render(guiGraphics, mouseX, mouseY, partialTick)
}
}
private fun updateScrollBounds(contentBottomAtZeroOffset: Int) {
minScrollOffset = minOf(0.0, (getOptionsBottom() - contentBottomAtZeroOffset).toDouble())
}
private fun getOptionsTop(): Int {
return headerHeight + optionsTopPadding
}
private fun getFooterTop(): Int {
return getScaledHeight() - footerHeight
}
private fun getOptionsBottom(): Int {
return getFooterTop() - optionsBottomPadding
}
private fun <T : AbstractWidget> addOptionWidget(widget: T): T {
optionWidgets.add(widget)
return addRenderableWidget(widget)
}
private fun <T : AbstractWidget> addFooterWidget(widget: T): T {
footerWidgets.add(widget)
return addRenderableWidget(widget)
}
private fun updateOptionWidgetInteractivity() {
val optionsTop = getOptionsTop()
val optionsBottom = getOptionsBottom()
optionWidgets.forEach { widget ->
val widgetTop = widget.y
val widgetBottom = widget.y + widget.height
val inViewport = widgetBottom > optionsTop && widgetTop < optionsBottom
widget.visible = inViewport
widget.active = inViewport
if (!inViewport) {
widget.setFocused(false)
}
}
}
fun getScaledWidth() = Minecraft.getInstance().window.guiScaledWidth
fun getScaledHeight() = Minecraft.getInstance().window.guiScaledHeight
}
@@ -0,0 +1,161 @@
/*
* 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.habitat
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatPool
import com.cobblemon.mod.common.api.habitats.NaturalHabitatPool
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import java.awt.Color
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.ObjectSelectionList
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
class HabitatPoolSelectionGUI(
private val isActivated: Boolean,
private val activatedPools: Map<ResourceLocation, ActivatedHabitatPool>,
private val naturalPools: Map<ResourceLocation, NaturalHabitatPool>,
private val selectedPoolId: ResourceLocation?,
private val parentScreen: Screen,
private val onSelect: (ResourceLocation?) -> Unit
) : Screen("Select Spawn Pool".text()), CobblemonRenderable {
private val headerHeight = 24
private val footerHeight = 44
private val listPadding = 6
private val backButtonHeight = 20
private val backButtonBottomMargin = 10
private lateinit var poolList: PoolList
override fun init() {
super.init()
val listTop = headerHeight + listPadding
val listBottom = getFooterTop() - listPadding
val listHeight = (listBottom - listTop).coerceAtLeast(24)
val listWidth = width - 20
val options = getOptions()
poolList = addRenderableWidget(
PoolList(
left = 10,
top = listTop,
listWidth = listWidth,
listHeight = listHeight,
options = options,
selectedPoolId = selectedPoolId,
onSelect = { poolId ->
onSelect(poolId)
Minecraft.getInstance().setScreen(parentScreen)
}
)
)
addRenderableWidget(
Button.builder("Back".text()) {
Minecraft.getInstance().setScreen(parentScreen)
}.bounds(10, height - (backButtonHeight + backButtonBottomMargin), listWidth, backButtonHeight).build()
)
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, width, height, Color(0, 0, 0, 100).rgb)
guiGraphics.fill(0, 0, width, headerHeight, Color(0, 0, 0, 150).rgb)
guiGraphics.fill(0, getFooterTop(), width, height, Color(0, 0, 0, 150).rgb)
super.render(guiGraphics, mouseX, mouseY, partialTick)
guiGraphics.drawCenteredString(font, title, width / 2, 8, 0xFFFFFF)
}
private fun getFooterTop(): Int {
return height - footerHeight
}
private fun getOptions(): List<Pair<ResourceLocation?, String>> {
val options = mutableListOf<Pair<ResourceLocation?, String>>()
options.add(null to "Custom")
val pools = if (isActivated) {
activatedPools.entries.map { it.key to it.value.name }
} else {
naturalPools.entries.map { it.key to it.value.name }
}.sortedBy { it.second.lowercase() }
options.addAll(pools)
return options
}
private class PoolList(
private val left: Int,
private val top: Int,
private val listWidth: Int,
private val listHeight: Int,
options: List<Pair<ResourceLocation?, String>>,
selectedPoolId: ResourceLocation?,
private val onSelect: (ResourceLocation?) -> Unit
) : ObjectSelectionList<PoolList.PoolEntry>(
Minecraft.getInstance(),
listWidth,
listHeight,
0,
20
), CobblemonRenderable {
init {
x = left
y = top
setRectangle(listWidth, listHeight, left, top)
options.forEach { (poolId, poolName) ->
addEntry(PoolEntry(poolId, poolName, onSelect))
}
setSelected(children().firstOrNull { it.poolId == selectedPoolId })
}
override fun renderListBackground(guiGraphics: GuiGraphics) {}
override fun getRowWidth(): Int {
return listWidth - 8
}
override fun getScrollbarPosition(): Int {
return left + listWidth - 6
}
private class PoolEntry(
val poolId: ResourceLocation?,
private val poolName: String,
private val onSelect: (ResourceLocation?) -> Unit
) : ObjectSelectionList.Entry<PoolEntry>() {
override fun render(
guiGraphics: GuiGraphics,
index: Int,
rowTop: Int,
rowLeft: Int,
rowWidth: Int,
rowHeight: Int,
mouseX: Int,
mouseY: Int,
hovered: Boolean,
partialTick: Float
) {
guiGraphics.drawString(Minecraft.getInstance().font, poolName, rowLeft + 4, rowTop + 6, 0xFFFFFF)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (!isMouseOver(mouseX, mouseY)) {
return false
}
onSelect(poolId)
return true
}
override fun getNarration(): Component {
return poolName.text()
}
}
}
}
@@ -0,0 +1,438 @@
/*
* 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.habitat
import com.cobblemon.mod.common.api.habitats.HabitatSpawn
import com.cobblemon.mod.common.api.habitats.NaturalHabitatSpawn
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.spawning.IntRanges
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.TimeRange
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.adapters.IntRangesAdapter
import com.cobblemon.mod.common.util.lang
import java.awt.Color
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.screens.Screen
import net.minecraft.client.resources.language.I18n
class HabitatSpawnEditorGUI(
val spawn: HabitatSpawn,
val isActivated: Boolean,
val maxPokemonLevel: Int,
val buckets: List<SpawnBucket>,
val spawnablePositionTypes: List<String>,
val parentScreen: Screen
) : Screen(lang("ui.edit.habitat.spawn")), CobblemonRenderable {
private var scrollOffset = 0.0
private var minScrollOffset = 0.0
private val headerHeight = 24
private val footerHeight = 44
private val optionsTopPadding = 6
private val optionsBottomPadding = 6
private val actionButtonHeight = 20
private val actionButtonBottomMargin = 10
private val validTextColor = 0xFFFFFF
private val invalidTextColor = 0xFF5555
private val moonPhaseRange = 0..7
private lateinit var positionTypeButton: Button
private lateinit var bucketButton: Button
private lateinit var timeOfDayButton: Button
private var timeOfDayIndex = 0
private val timeOfDayOptions: List<Pair<String, TimeRange?>> by lazy {
val options = mutableListOf<Pair<String, TimeRange?>>("Any" to null)
options.addAll(
TimeRange.timeRanges.entries
.filter { it.key != "any" }
.map { (key, value) -> key.replaceFirstChar(Char::uppercaseChar) to TimeRange(*value.ranges.toTypedArray()) }
)
options
}
private val optionWidgets = mutableListOf<AbstractWidget>()
private val footerWidgets = mutableListOf<AbstractWidget>()
private val validationErrors = linkedMapOf<String, String>()
private var validationMessageY = 0
private val labels = mutableListOf<Pair<String, Pair<Int, Int>>>()
val pokemonSpeciesByTranslatedName: Map<String, Species> by lazy {
PokemonSpecies.implemented.associateBy { I18n.get(it.translationKey)?.lowercase() ?: it.translationKey }
}
override fun init() {
super.init()
optionWidgets.clear()
footerWidgets.clear()
validationErrors.clear()
scrollOffset = scrollOffset.coerceIn(minScrollOffset, 0.0)
labels.clear()
var y = getOptionsTop() + scrollOffset.toInt()
val spacing = 25
val buttonWidth = getScaledWidth() - 20
labels.add("Species Name" to (10 to y))
y += 12
val speciesInput = EditBox(font, 10, y, buttonWidth, 20, "Species".text())
speciesInput.value = spawn.species.name
speciesInput.setResponder { value ->
val species = parseSpecies(value)
val valid = species != null
if (valid) {
spawn.species = species!!
}
speciesInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("species", valid, "ui.edit.habitat.validation.species")
}
speciesInput.setTextColor(validTextColor)
setFieldValidity("species", true, "ui.edit.habitat.validation.species")
addOptionWidget(speciesInput)
y += spacing
positionTypeButton = addOptionWidget(
Button.builder("Position Type: ${spawn.spawnablePositionType}".text()) {
val currentIndex = spawnablePositionTypes.indexOf(spawn.spawnablePositionType)
spawn.spawnablePositionType = spawnablePositionTypes[(currentIndex + 1) % spawnablePositionTypes.size]
refresh()
}.bounds(10, y, buttonWidth, 20).build()
)
y += spacing
if (spawn is NaturalHabitatSpawn) {
bucketButton = addOptionWidget(
Button.builder("Bucket: ${spawn.bucket.name}".text()) {
val currentIndex = buckets.indexOf(spawn.bucket)
spawn.bucket = buckets[(currentIndex + 1) % buckets.size]
refresh()
}.bounds(10, y, buttonWidth, 20).build()
)
y += spacing
}
labels.add("Weight" to (10 to y))
y += 12
val weightInput = EditBox(font, 10, y, buttonWidth, 20, "Weight".text())
weightInput.value = spawn.weight.toString()
weightInput.setResponder { value ->
val parsed = value.toFloatOrNull()
val valid = parsed != null && parsed > 0
if (valid) {
spawn.weight = parsed!!
}
weightInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("weight", valid, "ui.edit.habitat.validation.weight")
}
weightInput.setTextColor(validTextColor)
setFieldValidity("weight", true, "ui.edit.habitat.validation.weight")
addOptionWidget(weightInput)
y += spacing
labels.add("Level Range (Min - Max)" to (10 to y))
y += 12
val levelMinInput = EditBox(font, 10, y, buttonWidth / 2 - 5, 20, "Min Level".text())
levelMinInput.value = spawn.levelRange.first.toString()
val levelMaxInput = EditBox(font, 10 + buttonWidth / 2 + 5, y, buttonWidth / 2 - 5, 20, "Max Level".text())
levelMaxInput.value = (if (spawn.levelRange.last == Int.MAX_VALUE) maxPokemonLevel else spawn.levelRange.last).toString()
levelMinInput.setResponder { _ ->
validateSpawnLevelRange(levelMinInput, levelMaxInput)
}
levelMaxInput.setResponder { _ ->
validateSpawnLevelRange(levelMinInput, levelMaxInput)
}
addOptionWidget(levelMinInput)
addOptionWidget(levelMaxInput)
validateSpawnLevelRange(levelMinInput, levelMaxInput)
y += spacing
labels.add("Modifiers (e.g., shiny gender=male)" to (10 to y))
y += 12
val modifiersInput = EditBox(font, 10, y, buttonWidth, 20, "Modifiers".text())
modifiersInput.value = spawn.modifiers?.asString() ?: ""
modifiersInput.setResponder { value ->
if (value.isBlank()) {
spawn.modifiers = null
modifiersInput.setTextColor(validTextColor)
setFieldValidity("modifiers", true, "ui.edit.habitat.validation.modifiers")
} else {
val parsed = runCatching { PokemonProperties.parse(value) }.getOrNull()
val valid = parsed != null
if (valid) {
spawn.modifiers = parsed
}
modifiersInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("modifiers", valid, "ui.edit.habitat.validation.modifiers")
}
}
modifiersInput.setTextColor(validTextColor)
setFieldValidity("modifiers", true, "ui.edit.habitat.validation.modifiers")
addOptionWidget(modifiersInput)
y += spacing
labels.add("Phases (e.g., 1-3, 5)" to (10 to y))
y += 12
val phasesInput = EditBox(font, 10, y, buttonWidth, 20, "Phases".text())
phasesInput.value = spawn.phases?.let { IntRangesAdapter.basic.serialize(it) } ?: ""
phasesInput.setResponder { value ->
if (value.isBlank()) {
spawn.phases = null
phasesInput.setTextColor(validTextColor)
setFieldValidity("phases_list", true, "ui.edit.habitat.validation.phases_list")
} else {
val parsed = parseMoonPhases(value)
val valid = parsed != null
if (valid) {
spawn.phases = parsed
}
phasesInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("phases_list", valid, "ui.edit.habitat.validation.phases_list")
}
}
val initialPhasesValid = phasesInput.value.isBlank() || parseMoonPhases(phasesInput.value) != null
phasesInput.setTextColor(if (initialPhasesValid) validTextColor else invalidTextColor)
setFieldValidity("phases_list", initialPhasesValid, "ui.edit.habitat.validation.phases_list")
addOptionWidget(phasesInput)
y += spacing
labels.add("Time of Day" to (10 to y))
y += 12
timeOfDayIndex = getInitialTimeOfDayIndex()
timeOfDayButton = addOptionWidget(
Button.builder(getTimeOfDayText()) {
timeOfDayIndex = (timeOfDayIndex + 1) % timeOfDayOptions.size
applyTimeOfDaySelection()
it.message = getTimeOfDayText()
}.bounds(10, y, buttonWidth, 20).build()
)
applyTimeOfDaySelection()
y += spacing
labels.add("Min Light (0-15, optional)" to (10 to y))
y += 12
val minLightInput = EditBox(font, 10, y, buttonWidth, 20, "Min Light".text())
minLightInput.value = spawn.minLight?.toString() ?: ""
minLightInput.setResponder { value ->
val parsed = value.toIntOrNull()
val valid = value.isBlank() || (parsed != null && parsed in 0..15)
if (valid) {
spawn.minLight = parsed
}
minLightInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("min_light", valid, "ui.edit.habitat.validation.min_light")
}
minLightInput.setTextColor(validTextColor)
setFieldValidity("min_light", true, "ui.edit.habitat.validation.min_light")
addOptionWidget(minLightInput)
y += spacing
labels.add("Max Light (0-15, optional)" to (10 to y))
y += 12
val maxLightInput = EditBox(font, 10, y, buttonWidth, 20, "Max Light".text())
maxLightInput.value = spawn.maxLight?.toString() ?: ""
maxLightInput.setResponder { value ->
val parsed = value.toIntOrNull()
val valid = value.isBlank() || (parsed != null && parsed in 0..15)
if (valid) {
spawn.maxLight = parsed
}
maxLightInput.setTextColor(if (valid) validTextColor else invalidTextColor)
setFieldValidity("max_light", valid, "ui.edit.habitat.validation.max_light")
}
maxLightInput.setTextColor(validTextColor)
setFieldValidity("max_light", true, "ui.edit.habitat.validation.max_light")
addOptionWidget(maxLightInput)
y += spacing
val contentBottomAtZeroOffset = y - scrollOffset.toInt()
updateScrollBounds(contentBottomAtZeroOffset)
if (scrollOffset < minScrollOffset) {
scrollOffset = minScrollOffset
rebuildWidgets()
return
}
addFooterWidget(
Button.builder("Back".text()) {
Minecraft.getInstance().setScreen(parentScreen)
}.bounds(10, getScaledHeight() - (actionButtonHeight + actionButtonBottomMargin), buttonWidth, actionButtonHeight).build()
)
validationMessageY = getFooterTop() + 6
updateOptionWidgetInteractivity()
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
if (mouseY < getOptionsTop() || mouseY > getOptionsBottom()) {
return false
}
scrollOffset = (scrollOffset + scrollY * 10).coerceIn(minScrollOffset, 0.0)
rebuildWidgets()
return true
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, width, height, Color(0, 0, 0, 100).rgb)
guiGraphics.fill(0, 0, width, headerHeight, Color(0, 0, 0, 150).rgb)
guiGraphics.fill(0, getFooterTop(), width, height, Color(0, 0, 0, 150).rgb)
guiGraphics.drawCenteredString(font, title, width / 2, 8, 0xFFFFFF)
guiGraphics.enableScissor(0, getOptionsTop(), width, getOptionsBottom())
optionWidgets.forEach { widget ->
widget.render(guiGraphics, mouseX, mouseY, partialTick)
}
labels.forEach { (label, pos) ->
if (pos.second >= getOptionsTop() && pos.second + font.lineHeight <= getOptionsBottom()) {
guiGraphics.drawString(font, label, pos.first, pos.second, 0xFFFFFF)
}
}
guiGraphics.disableScissor()
footerWidgets.forEach { widget ->
widget.render(guiGraphics, mouseX, mouseY, partialTick)
}
validationErrors.values.firstOrNull()?.let { errorKey ->
guiGraphics.drawString(font, lang(errorKey), 10, validationMessageY, invalidTextColor)
}
}
fun refresh() {
positionTypeButton.message = "Position Type: ${spawn.spawnablePositionType}".text()
if (spawn is NaturalHabitatSpawn) {
bucketButton.message = "Bucket: ${spawn.bucket.name}".text()
}
}
// data validation for error handling
private fun validateSpawnLevelRange(levelMinInput: EditBox, levelMaxInput: EditBox) {
val min = levelMinInput.value.toIntOrNull()
val max = levelMaxInput.value.toIntOrNull()
val minInRange = min != null && min in 1..maxPokemonLevel
val maxInRange = max != null && max in 1..maxPokemonLevel
val ordered = min != null && max != null && min <= max
val valid = minInRange && maxInRange && ordered
val orderInvalid = minInRange && maxInRange && !ordered
levelMinInput.setTextColor(if (minInRange && !orderInvalid) validTextColor else invalidTextColor)
levelMaxInput.setTextColor(if (maxInRange && !orderInvalid) validTextColor else invalidTextColor)
if (valid) {
spawn.levelRange = min!!..(if (max == maxPokemonLevel) Int.MAX_VALUE else max!!)
}
setFieldValidity("level_range", valid, "ui.edit.habitat.validation.level_range")
}
private fun parseSpecies(value: String): Species? {
val lowered = value.lowercase()
pokemonSpeciesByTranslatedName[lowered]?.let { return it }
return runCatching { PokemonSpecies.getByName(lowered) }.getOrNull()
}
private fun parseMoonPhases(raw: String): IntRanges? {
val ranges = mutableListOf<IntRange>()
val tokens = raw.split(",").map(String::trim)
if (tokens.isEmpty() || tokens.any { it.isEmpty() }) {
return null
}
for (token in tokens) {
val parts = token.split("-").map(String::trim)
val range = when (parts.size) {
1 -> {
val value = parts[0].toIntOrNull() ?: return null
if (value !in moonPhaseRange) return null
value..value
}
2 -> {
val start = parts[0].toIntOrNull() ?: return null
val end = parts[1].toIntOrNull() ?: return null
if (start !in moonPhaseRange || end !in moonPhaseRange || start > end) {
return null
}
start..end
}
else -> return null
}
ranges.add(range)
}
return IntRanges(*ranges.toTypedArray())
}
private fun getInitialTimeOfDayIndex(): Int {
val current = spawn.timeRange ?: return 0
val serialized = TimeRange.adapter.serialize(current)
val matched = timeOfDayOptions.indexOfFirst { (_, value) ->
value != null && TimeRange.adapter.serialize(value) == serialized
}
return if (matched == -1) 0 else matched
}
private fun applyTimeOfDaySelection() {
spawn.timeRange = timeOfDayOptions[timeOfDayIndex].second?.let { TimeRange(*it.ranges.toTypedArray()) }
}
private fun getTimeOfDayText() = "Time of Day: ${timeOfDayOptions[timeOfDayIndex].first}".text()
private fun setFieldValidity(field: String, valid: Boolean, errorKey: String) {
if (valid) {
validationErrors.remove(field)
} else {
validationErrors[field] = errorKey
}
}
private fun updateScrollBounds(contentBottomAtZeroOffset: Int) {
minScrollOffset = minOf(0.0, (getOptionsBottom() - contentBottomAtZeroOffset).toDouble())
}
private fun getOptionsTop(): Int {
return headerHeight + optionsTopPadding
}
private fun getFooterTop(): Int {
return getScaledHeight() - footerHeight
}
private fun getOptionsBottom(): Int {
return getFooterTop() - optionsBottomPadding
}
private fun <T : AbstractWidget> addOptionWidget(widget: T): T {
optionWidgets.add(widget)
return addRenderableWidget(widget)
}
private fun <T : AbstractWidget> addFooterWidget(widget: T): T {
footerWidgets.add(widget)
return addRenderableWidget(widget)
}
private fun updateOptionWidgetInteractivity() {
val optionsTop = getOptionsTop()
val optionsBottom = getOptionsBottom()
optionWidgets.forEach { widget ->
val widgetTop = widget.y
val widgetBottom = widget.y + widget.height
val inViewport = widgetBottom > optionsTop && widgetTop < optionsBottom
widget.visible = inViewport
widget.active = inViewport
if (!inViewport) {
widget.setFocused(false)
}
}
}
fun getScaledWidth() = Minecraft.getInstance().window.guiScaledWidth
fun getScaledHeight() = Minecraft.getInstance().window.guiScaledHeight
}
@@ -29,7 +29,7 @@ object DebugKeybindings {
TranslateUpKeybinding(),
TranslateDownKeybinding(),
PrintModelSettingsKeybinding(),
ToggleRidingStatsDebugGUIKeybinding()
ToggleRidingStatsDebugGUIKeybinding(),
)
class ScaleUpKeybinding: CobblemonKeyBinding(
@@ -163,5 +163,4 @@ object DebugKeybindings {
}
}
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.habitat
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.habitat.HabitatEditGUI
import com.cobblemon.mod.common.net.messages.client.habitat.OpenHabitatBlockEditorPacket
import net.minecraft.client.Minecraft
object OpenHabitatBlockEditorHandler : ClientNetworkPacketHandler<OpenHabitatBlockEditorPacket> {
override fun handle(packet: OpenHabitatBlockEditorPacket, client: Minecraft) {
client.setScreen(
HabitatEditGUI(
blockPos = packet.blockPos,
currentlySpawned = packet.currentlySpawned,
currentPhase = packet.currentPhase,
maxPokemonLevel = packet.maxPokemonLevel,
spawnablePositionTypes = packet.spawnablePositionTypes,
buckets = packet.buckets,
activatedHabitatPools = packet.activatedHabitatPools,
naturalHabitatPools = packet.naturalHabitatPools,
habitatSettingsDTO = packet.habitatSettingsDTO
)
)
}
}
@@ -1,37 +0,0 @@
package com.cobblemon.mod.common.client.render.block
import com.cobblemon.mod.common.block.entity.GrottoBlockEntity
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider
import net.minecraft.world.level.block.state.BlockState
class GrottoBlockRenderer(ctx: BlockEntityRendererProvider.Context) : BlockEntityRenderer<GrottoBlockEntity> {
override fun render(
entity: GrottoBlockEntity,
partialTicks: Float,
poseStack: PoseStack,
bufferSource: MultiBufferSource,
packedLight: Int,
packedOverlay: Int
) {
val stateToRender: BlockState = entity.mimickedState
val level = entity.level ?: return
poseStack.pushPose()
Minecraft.getInstance().blockRenderer.renderBatched(
stateToRender,
entity.blockPos,
level,
poseStack,
bufferSource.getBuffer(RenderType.solid()),
true,
level.random
)
poseStack.popPose()
}
}
@@ -0,0 +1,245 @@
/*
* 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.render.block
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.math.Axis
import java.util.concurrent.ThreadLocalRandom
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sin
import net.minecraft.client.Minecraft
import net.minecraft.client.multiplayer.ClientLevel
import net.minecraft.client.renderer.ItemBlockRenderTypes
import net.minecraft.client.renderer.LevelRenderer
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth
import net.minecraft.world.level.block.state.BlockState
class HabitatBlockRenderer(ctx: BlockEntityRendererProvider.Context) : BlockEntityRenderer<HabitatBlockEntity> {
private companion object {
const val SPECIES_CYCLE_TICKS = 60L
const val VISUAL_Y_OFFSET = 0.22
const val BASE_SPAWN_DELAY = 20
// Extra per-renderer vertical tuning. Increase to raise the rendered model.
const val MODEL_Y_ADJUST = .3
// Extra global multiplier applied after the cage-fit algorithm.
const val MODEL_SCALE_MULTIPLIER = 0.6F
const val SPAWNER_X_TILT_DEGREES = -30.0F
const val FIT_PADDING = 0.95F
const val CAGE_MIN_XZ = 0.05
const val CAGE_MAX_XZ = 0.95
const val CAGE_MIN_Y = 0.50
const val CAGE_MAX_Y = 0.98
const val PARTICLE_POINTS_PER_TICK = 1
const val PARTICLE_EMIT_INTERVAL_TICKS = 2L
}
private data class CachedDisplayPokemon(
val level: ClientLevel,
val speciesId: ResourceLocation,
val pokemonEntity: PokemonEntity
)
private data class SpawnerSpinState(
var spin: Double = 0.0,
var oSpin: Double = 0.0,
var spawnDelay: Int = BASE_SPAWN_DELAY,
var lastTick: Long = Long.MIN_VALUE
)
private val cachedDisplayPokemon = mutableMapOf<Long, CachedDisplayPokemon>()
private val spinStateByPos = mutableMapOf<Long, SpawnerSpinState>()
private val lastParticleGameTimeByPos = mutableMapOf<Long, Long>()
override fun render(
entity: HabitatBlockEntity,
partialTicks: Float,
poseStack: PoseStack,
bufferSource: MultiBufferSource,
packedLight: Int,
packedOverlay: Int
) {
val player = Minecraft.getInstance().player
val viewerHoldingHabitatBlock = (player?.mainHandItem ?: player?.offhandItem)?.item == CobblemonItems.HABITAT_BLOCK
val stateToRender: BlockState = if (viewerHoldingHabitatBlock) entity.blockState else entity.mimickedState
val level = entity.level ?: return
val isSolidRender = if (viewerHoldingHabitatBlock) false else stateToRender.isSolidRender(level, entity.blockPos)
val renderType = if (viewerHoldingHabitatBlock) {
RenderType.cutout()
} else {
runCatching { ItemBlockRenderTypes.getChunkRenderType(stateToRender) }
.getOrElse { if (isSolidRender) RenderType.solid() else RenderType.translucent() }
}
poseStack.pushPose()
Minecraft.getInstance().blockRenderer.renderBatched(
stateToRender,
entity.blockPos,
level,
poseStack,
bufferSource.getBuffer(renderType),
false,
level.random
)
poseStack.popPose()
if (viewerHoldingHabitatBlock) {
renderSpawnerVisuals(entity, partialTicks, poseStack, bufferSource, packedLight)
}
}
private fun renderSpawnerVisuals(
habitatEntity: HabitatBlockEntity,
partialTicks: Float,
poseStack: PoseStack,
bufferSource: MultiBufferSource,
packedLight: Int
) {
val level = habitatEntity.level as? ClientLevel ?: return
val posKey = habitatEntity.blockPos.asLong()
val speciesIds = habitatEntity.displaySpeciesIds
if (speciesIds.isEmpty()) {
cachedDisplayPokemon.remove(posKey)
spinStateByPos.remove(posKey)
lastParticleGameTimeByPos.remove(posKey)
return
}
val cycleIndex = ((level.gameTime / SPECIES_CYCLE_TICKS) % speciesIds.size.toLong()).toInt()
val speciesId = speciesIds[cycleIndex]
val pokemonEntity = getOrCreateDisplayPokemon(level, posKey, speciesId) ?: return
val spinState = spinStateByPos.getOrPut(posKey) { SpawnerSpinState() }
tickSpinState(spinState, level.gameTime)
val interpolatedSpin = Mth.rotLerp(partialTicks, spinState.oSpin.toFloat(), spinState.spin.toFloat()) * 10.0F
pokemonEntity.tickCount = (level.gameTime and Int.MAX_VALUE.toLong()).toInt()
pokemonEntity.setPos(
habitatEntity.blockPos.x + 0.5,
habitatEntity.blockPos.y + VISUAL_Y_OFFSET + MODEL_Y_ADJUST + 0.4,
habitatEntity.blockPos.z + 0.5
)
val entityLight = LevelRenderer.getLightColor(level, habitatEntity.blockPos.above())
poseStack.pushPose()
poseStack.translate(0.5, VISUAL_Y_OFFSET + MODEL_Y_ADJUST, 0.5)
val scale = computeFittedModelScale(pokemonEntity)
poseStack.translate(0.0, 0.4, 0.0)
poseStack.mulPose(Axis.YP.rotationDegrees(interpolatedSpin))
poseStack.translate(0.0, -0.2, 0.0)
poseStack.mulPose(Axis.XP.rotationDegrees(SPAWNER_X_TILT_DEGREES))
poseStack.scale(scale, scale, scale)
val dispatcher = Minecraft.getInstance().entityRenderDispatcher
dispatcher.setRenderShadow(false)
dispatcher.render(pokemonEntity, 0.0, 0.0, 0.0, 0F, partialTicks, poseStack, bufferSource, entityLight)
dispatcher.setRenderShadow(true)
poseStack.popPose()
emitSpawnerParticles(level, posKey, habitatEntity.blockPos.x, habitatEntity.blockPos.y, habitatEntity.blockPos.z)
}
private fun getOrCreateDisplayPokemon(level: ClientLevel, posKey: Long, speciesId: ResourceLocation): PokemonEntity? {
val cached = cachedDisplayPokemon[posKey]
if (cached != null && cached.level === level && cached.speciesId == speciesId) {
return cached.pokemonEntity
}
val species = PokemonSpecies.getByIdentifier(speciesId) ?: return null
val pokemon = Pokemon().apply {
isClient = true
this.species = species
initialize()
}
val pokemonEntity = PokemonEntity(level, pokemon).apply {
noPhysics = true
setNoGravity(true)
}
cachedDisplayPokemon[posKey] = CachedDisplayPokemon(level, speciesId, pokemonEntity)
return pokemonEntity
}
private fun emitSpawnerParticles(level: ClientLevel, posKey: Long, x: Int, y: Int, z: Int) {
val gameTime = level.gameTime
if (lastParticleGameTimeByPos[posKey] == gameTime || gameTime % PARTICLE_EMIT_INTERVAL_TICKS != 0L) {
return
}
lastParticleGameTimeByPos[posKey] = gameTime
val rng = ThreadLocalRandom.current()
repeat(PARTICLE_POINTS_PER_TICK) {
val px = x + rng.nextDouble(CAGE_MIN_XZ, CAGE_MAX_XZ)
val py = y + rng.nextDouble(CAGE_MIN_Y, CAGE_MAX_Y)
val pz = z + rng.nextDouble(CAGE_MIN_XZ, CAGE_MAX_XZ)
level.addParticle(ParticleTypes.SMOKE, px, py, pz, 0.0, 0.0, 0.0)
level.addParticle(ParticleTypes.FLAME, px, py, pz, 0.0, 0.0, 0.0)
}
}
private fun computeFittedModelScale(pokemonEntity: PokemonEntity): Float {
val rawWidth = pokemonEntity.bbWidth.coerceAtLeast(0.05F)
val rawDepth = rawWidth
val rawHeight = pokemonEntity.bbHeight.coerceAtLeast(0.05F)
// Volume-based baseline shrink so bulkier mons are reduced by their full hitbox mass,
// rather than only the single largest dimension.
var vanillaScale = 0.53125F
val hitboxVolume = rawWidth * rawDepth * rawHeight
if (hitboxVolume > 1.0F) {
vanillaScale /= hitboxVolume.pow(1F / 3F)
}
// Fit to upper-cage bounds so large mons stay inside the top half.
val availableWidth = (CAGE_MAX_XZ - CAGE_MIN_XZ).toFloat()
val availableDepth = (CAGE_MAX_XZ - CAGE_MIN_XZ).toFloat()
val availableHeight = (CAGE_MAX_Y - CAGE_MIN_Y).toFloat()
val tiltRadians = Math.toRadians(abs(SPAWNER_X_TILT_DEGREES.toDouble()))
val c = cos(tiltRadians).toFloat()
val s = sin(tiltRadians).toFloat()
// Conservative AABB of the model after the vanilla spawner X-tilt.
val tiltedDepth = rawDepth * c + rawHeight * s
val tiltedHeight = rawHeight * c + rawDepth * s
val fitScaleX = availableWidth / rawWidth
val fitScaleZ = availableDepth / tiltedDepth.coerceAtLeast(0.05F)
val fitScaleY = availableHeight / tiltedHeight.coerceAtLeast(0.05F)
val fitScale = min(min(fitScaleX, fitScaleZ), fitScaleY) * FIT_PADDING
return (min(vanillaScale, fitScale) * MODEL_SCALE_MULTIPLIER).coerceAtLeast(0.01F)
}
private fun tickSpinState(spinState: SpawnerSpinState, gameTime: Long) {
if (spinState.lastTick == Long.MIN_VALUE) {
spinState.lastTick = gameTime - 1
}
while (spinState.lastTick < gameTime) {
spinState.lastTick++
spinState.oSpin = spinState.spin
if (spinState.spawnDelay > 0) {
spinState.spawnDelay--
}
spinState.spin = (spinState.spin + 1000.0 / (spinState.spawnDelay.toDouble() + 200.0)) % 360.0
}
}
}
@@ -8,17 +8,8 @@
package com.cobblemon.mod.common.data
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.*
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.CobblemonBehaviours
import com.cobblemon.mod.common.CobblemonCallbacks
import com.cobblemon.mod.common.CobblemonCosmeticItems
import com.cobblemon.mod.common.CobblemonMechanics
import com.cobblemon.mod.common.CobblemonMovesetBuilders
import com.cobblemon.mod.common.CobblemonPartyCompositions
import com.cobblemon.mod.common.CobblemonPartyPools
import com.cobblemon.mod.common.CobblemonRideSettings
import com.cobblemon.mod.common.CobblemonUnlockableWallpapers
import com.cobblemon.mod.common.api.abilities.Abilities
import com.cobblemon.mod.common.api.berry.Berries
import com.cobblemon.mod.common.api.cooking.Seasonings
@@ -31,6 +22,7 @@ import com.cobblemon.mod.common.api.fishing.SpawnBait
import com.cobblemon.mod.common.api.fishing.SpawnBaitEffects
import com.cobblemon.mod.common.api.fossil.Fossils
import com.cobblemon.mod.common.api.fossil.NaturalMaterials
import com.cobblemon.mod.common.api.habitats.HabitatPools
import com.cobblemon.mod.common.api.interaction.PokemonInteractions
import com.cobblemon.mod.common.api.item.HeldItems
import com.cobblemon.mod.common.api.mark.Marks
@@ -114,6 +106,7 @@ object CobblemonDataProvider : DataProvider {
CobblemonSpawnPools.load()
this.register(PokeRods, reloadable = false)
this.register(Berries, reloadable = false)
this.register(HabitatPools, reloadable = true)
this.register(Seasonings, reloadable = false)
this.register(PokemonInteractions, reloadable = false)
this.register(SpawnBaitEffects, reloadable = false)
@@ -23,9 +23,6 @@ import com.cobblemon.mod.common.api.spawning.BestSpawner
import com.cobblemon.mod.common.api.spawning.detail.EntitySpawnResult
import com.cobblemon.mod.common.api.spawning.detail.SpawnAction
import com.cobblemon.mod.common.api.spawning.fishing.FishingSpawnCause
import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
import com.cobblemon.mod.common.api.spawning.position.FishingSpawnablePosition
import com.cobblemon.mod.common.api.spawning.spawner.FishingSpawnerFactory
import com.cobblemon.mod.common.api.text.red
@@ -392,7 +392,7 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.WATER_STONE_ORE)
entries.accept(CobblemonItems.DEEPSLATE_WATER_STONE_ORE)
entries.accept(CobblemonItems.GROTTO_BLOCK)
entries.accept(CobblemonItems.HABITAT_BLOCK)
entries.accept(CobblemonItems.DAWN_STONE_BLOCK)
entries.accept(CobblemonItems.DUSK_STONE_BLOCK)
entries.accept(CobblemonItems.FIRE_STONE_BLOCK)
@@ -0,0 +1,129 @@
/*
* 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.habitat
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.habitats.ActivatedHabitatPool
import com.cobblemon.mod.common.api.habitats.HabitatPools
import com.cobblemon.mod.common.api.habitats.NaturalHabitatPool
import com.cobblemon.mod.common.api.habitats.dto.HabitatSettingsDTO
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readMapK
import com.cobblemon.mod.common.util.writeMapK
import net.minecraft.core.BlockPos
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* Opens the habitat block editor GUI with information about the habitat pools to choose from as well as the current
* settings. The client will then send a SaveHabitatBlockSettingsPacket with the modified settings.
*
* @author Hiroku
* @since February 21st, 2026
*/
class OpenHabitatBlockEditorPacket(
val blockPos: BlockPos,
val currentlySpawned: Int,
val currentPhase: Int,
val maxPokemonLevel: Int,
val spawnablePositionTypes: List<String>,
val buckets: List<SpawnBucket>,
val activatedHabitatPools: Map<ResourceLocation, ActivatedHabitatPool>,
val naturalHabitatPools: Map<ResourceLocation, NaturalHabitatPool>,
val habitatSettingsDTO: HabitatSettingsDTO
) : NetworkPacket<OpenHabitatBlockEditorPacket> {
companion object {
val ID = cobblemonResource("open_habitat_block_editor")
fun decode(buffer: RegistryFriendlyByteBuf): OpenHabitatBlockEditorPacket {
val blockPos = buffer.readBlockPos()
val currentlySpawned = buffer.readInt()
val currentPhase = buffer.readInt()
val maxPokemonLevel = buffer.readInt()
val spawnablePositionTypes = buffer.readList { buffer.readUtf() }
val buckets = buffer.readList {
val name = buffer.readUtf()
val weight = buffer.readFloat()
SpawnBucket(name = name, weight = weight)
}
val activatedHabitatPools = buffer.readMapK(
size = IntSize.U_SHORT,
mutableMapOf(),
entryReader = {
val pool = ActivatedHabitatPool()
pool.decode(buffer, buckets)
pool.id to pool
}
)
val naturalHabitatPools = buffer.readMapK(
size = IntSize.U_SHORT,
mutableMapOf(),
entryReader = {
val pool = NaturalHabitatPool()
pool.decode(buffer, buckets)
pool.id to pool
}
)
val habitatSettingsDTO = HabitatSettingsDTO()
habitatSettingsDTO.decode(buffer, buckets)
return OpenHabitatBlockEditorPacket(
blockPos = blockPos,
currentlySpawned = currentlySpawned,
currentPhase = currentPhase,
maxPokemonLevel = maxPokemonLevel,
spawnablePositionTypes = spawnablePositionTypes,
buckets = buckets,
activatedHabitatPools = activatedHabitatPools,
naturalHabitatPools = naturalHabitatPools,
habitatSettingsDTO = habitatSettingsDTO
)
}
}
constructor(habitatBlockEntity: HabitatBlockEntity): this(
blockPos = habitatBlockEntity.blockPos,
currentlySpawned = habitatBlockEntity.spawnedEntityIDs.size,
currentPhase = habitatBlockEntity.currentPhase,
maxPokemonLevel = Cobblemon.config.maxPokemonLevel,
spawnablePositionTypes = SpawnablePosition.spawnablePositionTypes.map { it.name },
buckets = Cobblemon.bestSpawner.config.buckets,
habitatSettingsDTO = HabitatSettingsDTO(habitatBlockEntity),
activatedHabitatPools = HabitatPools.habitatPoolsById.entries.filter { it.value is ActivatedHabitatPool }.associate { it.key to it.value as ActivatedHabitatPool },
naturalHabitatPools = HabitatPools.habitatPoolsById.entries.filter { it.value is NaturalHabitatPool }.associate { it.key to it.value as NaturalHabitatPool }
)
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBlockPos(blockPos)
buffer.writeInt(currentlySpawned)
buffer.writeInt(currentPhase)
buffer.writeInt(maxPokemonLevel)
buffer.writeCollection(spawnablePositionTypes) { _, it -> buffer.writeUtf(it) }
buffer.writeCollection(buckets) { _, bucket ->
buffer.writeUtf(bucket.name)
buffer.writeFloat(bucket.weight)
}
buffer.writeMapK(
size = IntSize.U_SHORT,
map = activatedHabitatPools,
entryWriter = { (_, value) -> value.encode(buffer) },
)
buffer.writeMapK(
size = IntSize.U_SHORT,
map = naturalHabitatPools,
entryWriter = { (_, value) -> value.encode(buffer) },
)
habitatSettingsDTO.encode(buffer)
}
}
@@ -1,23 +0,0 @@
package com.cobblemon.mod.common.net.messages.server.debug
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 UpdateGrottoBlockPacket(
val grottoPos: BlockPos
) : NetworkPacket<UpdateGrottoBlockPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBlockPos(grottoPos)
}
companion object {
val ID = cobblemonResource("c2s_update_grotto_block")
fun decode(buffer: RegistryFriendlyByteBuf): UpdateGrottoBlockPacket =
UpdateGrottoBlockPacket(
grottoPos = buffer.readBlockPos(),
)
}
}
@@ -0,0 +1,46 @@
/*
* 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.habitat
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.habitats.dto.HabitatSettingsDTO
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
/**
* Sent from the client to the server with the new settings to apply to the habitat block at that position.
*
* @author Hiroku
* @since February 21st, 2026
*/
class SaveHabitatBlockSettingsPacket(
val blockPos: BlockPos,
val settings: HabitatSettingsDTO
) : NetworkPacket<SaveHabitatBlockSettingsPacket> {
companion object {
val ID = cobblemonResource("save_habitat_block_settings")
fun decode(buffer: RegistryFriendlyByteBuf): SaveHabitatBlockSettingsPacket {
val blockPos = buffer.readBlockPos()
val settings = HabitatSettingsDTO()
settings.decode(
buffer = buffer,
buckets = Cobblemon.bestSpawner.config.buckets
)
return SaveHabitatBlockSettingsPacket(blockPos, settings)
}
}
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBlockPos(blockPos)
settings.encode(buffer)
}
}
@@ -1,18 +0,0 @@
package com.cobblemon.mod.common.net.serverhandling.debug
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.block.entity.GrottoBlockEntity
import com.cobblemon.mod.common.net.messages.server.debug.UpdateGrottoBlockPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object UpdateGrottoBlockHandler : ServerNetworkPacketHandler<UpdateGrottoBlockPacket> {
override fun handle(packet: UpdateGrottoBlockPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
val block = player.level().getBlockEntity(packet.grottoPos) ?: return
if (block !is GrottoBlockEntity) return
}
}
@@ -0,0 +1,48 @@
/*
* 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.habitat
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.habitats.SaveHabitatBlockSettingsEvent
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.block.habitat.HabitatBlockEntity
import com.cobblemon.mod.common.net.messages.server.habitat.SaveHabitatBlockSettingsPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object SaveHabitatBlockSettingsHandler : ServerNetworkPacketHandler<SaveHabitatBlockSettingsPacket> {
override fun handle(packet: SaveHabitatBlockSettingsPacket, server: MinecraftServer, player: ServerPlayer) {
val world = player.serverLevel()
val blockEntity = world.getBlockEntity(packet.blockPos) as? HabitatBlockEntity ?: return // it's broken
val pre = SaveHabitatBlockSettingsEvent.Pre(
player = player,
world = world,
blockPos = packet.blockPos,
habitatBlockEntity = blockEntity,
settings = packet.settings
)
if (!player.isCreative) {
// mf does not have permission
return
}
CobblemonEvents.HABITAT_SETTINGS_SAVED_PRE.postThen(pre) {
blockEntity.applySettings(habitatSettingsDTO = packet.settings)
val post = SaveHabitatBlockSettingsEvent.Post(
player = player,
world = world,
blockPos = packet.blockPos,
habitatBlockEntity = blockEntity,
settings = packet.settings
)
CobblemonEvents.HABITAT_SETTINGS_SAVED_POST.post(post)
}
}
}
@@ -43,16 +43,7 @@ import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
import com.cobblemon.mod.common.pokemon.ai.PokemonBehaviour
import com.cobblemon.mod.common.pokemon.lighthing.LightingData
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readEntityDimensions
import com.cobblemon.mod.common.util.readEnumConstant
import com.cobblemon.mod.common.util.readIdentifier
import com.cobblemon.mod.common.util.readSizedInt
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeEnumConstant
import com.cobblemon.mod.common.util.writeIdentifier
import com.cobblemon.mod.common.util.writeSizedInt
import com.cobblemon.mod.common.util.writeString
import com.cobblemon.mod.common.util.*
import com.mojang.serialization.Codec
import com.mojang.serialization.DataResult
import net.minecraft.network.RegistryFriendlyByteBuf
@@ -63,8 +54,10 @@ import net.minecraft.world.entity.EntityDimensions
class Species : ClientDataSynchronizer<Species>, ShowdownIdentifiable {
var name: String = "Bulbasaur"
val translationKey: String
get() = "${this.resourceIdentifier.namespace}.species.${this.unformattedShowdownId()}.name"
val translatedName: MutableComponent
get() = Component.translatable("${this.resourceIdentifier.namespace}.species.${this.unformattedShowdownId()}.name")
get() = Component.translatable(translationKey)
var nationalPokedexNumber = 1
var baseStats = hashMapOf<Stat, Int>()
@@ -269,4 +269,41 @@ object DataKeys {
// PokemonProperties
const val ELEMENTAL_TYPE = "ElementalType"
// Habitat Blocks
const val HABITAT_SPAWNING_STYLE = "SpawningStyle"
const val HABITAT_MIMIC = "MimicId"
const val HABITAT_PHASE_COUNT = "PhaseCount"
const val HABITAT_PHASE_ORDER = "PhaseOrder"
const val HABITAT_LEVEL_RANGE = "LevelRange"
const val HABITAT_MODIFIERS = "Modifiers"
const val HABITAT_POOL = "Pool"
// --- Pools
const val HABITAT_POOL_ID = "PoolId"
const val HABITAT_POOL_NAME = "Name"
const val HABITAT_POOL_SPAWNS = "Spawns"
const val HABITAT_POOL_SPAWN_SPECIES = "Species"
const val HABITAT_POOL_SPAWN_MODIFIERS = "Modifiers"
const val HABITAT_POOL_SPAWN_LEVEL_RANGE = "LevelRange"
const val HABITAT_POOL_SPAWN_WEIGHT = "Weight"
const val HABITAT_POOL_SPAWN_POSITION_TYPE = "PositionType"
const val HABITAT_POOL_SPAWN_TIMES = "Times"
const val HABITAT_POOL_SPAWN_PHASES = "Phases"
const val HABITAT_POOL_SPAWN_MIN_LIGHT = "MinLight"
const val HABITAT_POOL_SPAWN_MAX_LIGHT = "MaxLight"
const val HABITAT_POOL_SPAWN_BUCKET = "Bucket"
// --- Activated
const val HABITAT_ACTIVATED_TRIGGER = "Trigger"
const val HABITAT_ACTIVATED_CHANCE = "Chance"
const val HABITAT_ACTIVATED_MAX_SPAWNS = "MaxSpawns"
const val HABITAT_ACTIVATED_MAX_SPAWNS_PER_ACTIVATION = "MaxSpawnsPerActivation"
const val HABITAT_ACTIVATED_SPAWN_RANGE = "SpawnRange"
const val HABITAT_ACTIVATED_CANCEL_RANGE = "CancelRange"
// --- Natural
const val HABITAT_NATURAL_REPLACE_SPAWNS = "ReplaceSpawns"
const val HABITAT_NATURAL_RANGE_OF_INFLUENCE = "RangeOfInfluence"
}
@@ -70,12 +70,13 @@ fun <K, V> ByteBuf.writeMapK(size: IntSize = IntSize.U_BYTE, map: Map<K, V>, ent
map.entries.forEach(entryWriter)
}
fun <K, V> ByteBuf.readMapK(size: IntSize = IntSize.U_BYTE, map: MutableMap<K, V>, entryReader: () -> Pair<K, V>) {
fun <K, V> ByteBuf.readMapK(size: IntSize = IntSize.U_BYTE, map: MutableMap<K, V>, entryReader: () -> Pair<K, V>): MutableMap<K, V> {
val times = readSizedInt(size)
repeat(times) {
val (key, value) = entryReader()
map[key] = value
}
return map
}
fun ByteBuf.writeVec3d(vec3d: Vec3) {
@@ -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.util.adapters
import com.cobblemon.mod.common.api.habitats.HabitatPool
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import java.lang.reflect.Type
/**
* Simple map adapter for which type of HabitatPool is being deserialized.
*
* @author Hiroku
* @since February 13th, 2026
*/
object HabitatPoolAdapter : JsonDeserializer<HabitatPool<*>> {
override fun deserialize(json: JsonElement, t: Type, ctx: JsonDeserializationContext): HabitatPool<*>? {
json as JsonObject
val type = json.get("type").asString.asIdentifierDefaultingNamespace()
val clazz = HabitatPool.types[type] ?: throw IllegalArgumentException("Unknown habitat pool type: $type")
return ctx.deserialize(json, clazz)
}
}
@@ -28,16 +28,18 @@ object IntRangeAdapter : JsonSerializer<IntRange>, JsonDeserializer<IntRange> {
private val PATTERN = "(-?\\d+)-?(-?\\d+)?".toRegex()
override fun serialize(range: IntRange, type: Type, ctx: JsonSerializationContext): JsonElement {
override fun serialize(range: IntRange, type: Type, ctx: JsonSerializationContext) = JsonPrimitive(serialize(range))
fun serialize(range: IntRange): String {
return if (range.first == range.last) {
JsonPrimitive(range.first)
range.first.toString()
} else {
JsonPrimitive("${range.first}-${range.last}")
"${range.first}-${range.last}"
}
}
override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): IntRange {
val (start, end) = PATTERN.find(json.asString)!!.destructured
override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext) = deserialize(json.asString)
fun deserialize(string: String): IntRange {
val (start, end) = PATTERN.find(string)!!.destructured
return if (end.isEmpty()) {
IntRange(start.toInt(), start.toInt())
} else {
@@ -29,20 +29,22 @@ import java.lang.reflect.Type
* @since January 26th, 2022
*/
class IntRangesAdapter<T : IntRanges>(val ranges: Map<String, T>, val initializer: (Array<IntRange>) -> T) : JsonDeserializer<T>, JsonSerializer<T> {
override fun serialize(src: T, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
return JsonPrimitive(src.ranges.joinToString { "${it.first}-${it.last}" })
companion object {
val basic = IntRangesAdapter(emptyMap()) { IntRanges(*it) }
}
override fun deserialize(json: JsonElement, t: Type, ctx: JsonDeserializationContext): T {
val str = json.asString
val splits = str.split(",")
fun serialize(src: T) = src.ranges.joinToString { if (it.first == it.last) it.first.toString() else "${it.first}-${it.last}" }
override fun serialize(src: T, typeOfSrc: Type?, context: JsonSerializationContext?) = JsonPrimitive(serialize(src))
fun deserialize(string: String): T {
val splits = string.split(",")
if (splits.isEmpty()) {
return initializer(emptyArray())
}
val ranges = mutableListOf<IntRange>()
splits.forEach {
val range = it.split("-")
val range = it.split("-").map { it.trim() }
if (range.size == 2 && range[0].isInt() && range[1].isInt()) {
ranges.add(range[0].toInt()..range[1].toInt())
} else if (range.size == 1) {
@@ -56,4 +58,5 @@ class IntRangesAdapter<T : IntRanges>(val ranges: Map<String, T>, val initialize
}
return initializer(ranges.toTypedArray())
}
override fun deserialize(json: JsonElement, t: Type, ctx: JsonDeserializationContext) = deserialize(json.asString)
}
@@ -0,0 +1,34 @@
/*
* 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.util.adapters
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
/**
* Deserializes a [Species] from a string property that is the identifier of the species.
*
* @author Hiroku
* @since February 13th, 2026
*/
object SpeciesAdapter : JsonSerializer<Species>, JsonDeserializer<Species> {
override fun serialize(src: Species, t: Type, ctx: JsonSerializationContext) = JsonPrimitive(src.resourceIdentifier.toString())
override fun deserialize(json: JsonElement, t: Type, ctx: JsonDeserializationContext): Species {
val speciesId = json.asString.asIdentifierDefaultingNamespace()
return PokemonSpecies.getByIdentifier(speciesId) ?: throw IllegalArgumentException("No such species: $speciesId")
}
}
@@ -1,5 +0,0 @@
{
"variants": {
"": { "model": "minecraft:block/air" }
}
}
@@ -0,0 +1,64 @@
{
"variants": {
"activated_style=false,facing=east,cancels_regular_spawns=false": {
"model": "cobblemon:block/habitat_block_natural",
"y": 90
},
"activated_style=false,facing=east,cancels_regular_spawns=true": {
"model": "cobblemon:block/habitat_block_natural_replace",
"y": 90
},
"activated_style=false,facing=north,cancels_regular_spawns=false": {
"model": "cobblemon:block/habitat_block_natural"
},
"activated_style=false,facing=north,cancels_regular_spawns=true": {
"model": "cobblemon:block/habitat_block_natural_replace"
},
"activated_style=false,facing=south,cancels_regular_spawns=false": {
"model": "cobblemon:block/habitat_block_natural",
"y": 180
},
"activated_style=false,facing=south,cancels_regular_spawns=true": {
"model": "cobblemon:block/habitat_block_natural_replace",
"y": 180
},
"activated_style=false,facing=west,cancels_regular_spawns=false": {
"model": "cobblemon:block/habitat_block_natural",
"y": 270
},
"activated_style=false,facing=west,cancels_regular_spawns=true": {
"model": "cobblemon:block/habitat_block_natural_replace",
"y": 270
},
"activated_style=true,facing=east,cancels_regular_spawns=false": {
"model": "cobblemon:block/habitat_block_activate",
"y": 90
},
"activated_style=true,facing=east,cancels_regular_spawns=true": {
"model": "cobblemon:block/habitat_block_activate_replace",
"y": 90
},
"activated_style=true,facing=north,cancels_regular_spawns=false": {
"model": "cobblemon:block/habitat_block_activate"
},
"activated_style=true,facing=north,cancels_regular_spawns=true": {
"model": "cobblemon:block/habitat_block_activate_replace"
},
"activated_style=true,facing=south,cancels_regular_spawns=false": {
"model": "cobblemon:block/habitat_block_activate",
"y": 180
},
"activated_style=true,facing=south,cancels_regular_spawns=true": {
"model": "cobblemon:block/habitat_block_activate_replace",
"y": 180
},
"activated_style=true,facing=west,cancels_regular_spawns=false": {
"model": "cobblemon:block/habitat_block_activate",
"y": 270
},
"activated_style=true,facing=west,cancels_regular_spawns=true": {
"model": "cobblemon:block/habitat_block_activate_replace",
"y": 270
}
}
}
@@ -1191,6 +1191,8 @@
"cobblemon.container.campfire_pot.recipe_book.toggle_recipes": "Showing Cookable",
"cobblemon.container.tm_machine": "TM Machine",
"cobblemon.ui.edit.habitat": "Habitat Editor",
"cobblemon.ui.pokenav.title": "PokéNav",
"cobblemon.ui.pokenav.narrator.backbutton": "Poke Nav button pressed",
@@ -1304,6 +1306,20 @@
"cobblemon.ui.sort.type": "Sort by type",
"cobblemon.ui.sort.discovered": "Sort by discovered status",
"cobblemon.ui.edit.habitat.validation.mimic": "Mimic block must be a valid allowed block ID (check habitat_mimics.json).",
"cobblemon.ui.edit.habitat.validation.phases": "Number of phases must be greater than 0.",
"cobblemon.ui.edit.habitat.validation.level_range": "Level range must be between 1 and 100, and Min cannot exceed Max.",
"cobblemon.ui.edit.habitat.validation.spawn_range": "Spawn range must be 0 or higher.",
"cobblemon.ui.edit.habitat.validation.cancelled_range": "Cancelled natural spawning range must be -1 (no cancellation) or between 0 and 64.",
"cobblemon.ui.edit.habitat.validation.max_spawns": "Max spawns must be -1 or higher.",
"cobblemon.ui.edit.habitat.validation.influence_range": "Range of influence must be 0 or higher.",
"cobblemon.ui.edit.habitat.validation.species": "Species must be a valid Pokemon name.",
"cobblemon.ui.edit.habitat.validation.weight": "Weight must be greater than 0.",
"cobblemon.ui.edit.habitat.validation.modifiers": "Modifiers format is invalid.",
"cobblemon.ui.edit.habitat.validation.phases_list": "Phases must use valid moon phases (0-7), e.g. 1-3 or 1, 3-5, 6.",
"cobblemon.ui.edit.habitat.validation.min_light": "Min Light must be between 0 and 15.",
"cobblemon.ui.edit.habitat.validation.max_light": "Max Light must be between 0 and 15.",
"cobblemon.ui.pasture": "Pasture",
"cobblemon.ui.pasture.recall_all": "Recall All",
"cobblemon.pasture.too_many_nearby": "There are too many Pokémon nearby.",
@@ -3596,6 +3612,8 @@
"block.cobblemon.poke_cake": "Poké Cake",
"block.cobblemon.poke_snack": "Poké Snack",
"block.cobblemon.habitat_block": "Habitat Block",
"block.cobblemon.white_plaque": "White Plaque",
"block.cobblemon.light_gray_plaque": "Light Gray Plaque",
"block.cobblemon.gray_plaque": "Gray Plaque",
@@ -1,7 +0,0 @@
{
"parent": "block/cube_all",
"textures": {
"all": "minecraft:block/air"
},
"ambientocclusion": true
}
@@ -0,0 +1,43 @@
{
"format_version": "1.21.11",
"credit": "Made with Blockbench",
"texture_size": [64, 32],
"textures": {
"particle": "cobblemon:block/functional/habitat_block"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"faces": {
"north": {"uv": [4, 0, 8, 8], "texture": "#particle"},
"east": {"uv": [12, 0, 8, 8], "texture": "#particle"},
"south": {"uv": [12, 0, 16, 8], "texture": "#particle"},
"west": {"uv": [8, 0, 12, 8], "texture": "#particle"},
"up": {"uv": [8, 8, 12, 16], "texture": "#particle"},
"down": {"uv": [12, 8, 16, 16], "texture": "#particle"}
}
},
{
"from": [0.5, 5.5, 0.025],
"to": [3.5, 8.5, 0.025],
"faces": {
"north": {"uv": [0.5, 0, 1, 1], "texture": "#particle"}
}
},
{
"from": [12.5, 5.5, 15.975],
"to": [15.5, 8.5, 15.975],
"faces": {
"south": {"uv": [0.5, 0, 1, 1], "texture": "#particle"}
}
},
{
"from": [0, 10, 0],
"to": [16, 10, 16],
"faces": {
"up": {"uv": [4, 8, 8, 16], "texture": "#particle"}
}
}
]
}
@@ -0,0 +1,43 @@
{
"format_version": "1.21.11",
"credit": "Made with Blockbench",
"texture_size": [64, 32],
"textures": {
"particle": "cobblemon:block/functional/habitat_block"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"faces": {
"north": {"uv": [4, 0, 8, 8], "texture": "#particle"},
"east": {"uv": [12, 0, 8, 8], "texture": "#particle"},
"south": {"uv": [12, 0, 16, 8], "texture": "#particle"},
"west": {"uv": [8, 0, 12, 8], "texture": "#particle"},
"up": {"uv": [8, 8, 12, 16], "texture": "#particle"},
"down": {"uv": [12, 8, 16, 16], "texture": "#particle"}
}
},
{
"from": [0.5, 5.5, 0.025],
"to": [3.5, 8.5, 0.025],
"faces": {
"north": {"uv": [0.5, 0, 1, 1], "texture": "#particle"}
}
},
{
"from": [12.5, 5.5, 15.975],
"to": [15.5, 8.5, 15.975],
"faces": {
"south": {"uv": [0.5, 0, 1, 1], "texture": "#particle"}
}
},
{
"from": [0, 10, 0],
"to": [16, 10, 16],
"faces": {
"up": {"uv": [0, 8, 4, 16], "texture": "#particle"}
}
}
]
}
@@ -0,0 +1,74 @@
{
"format_version": "1.21.11",
"credit": "Made with Blockbench",
"texture_size": [64, 32],
"textures": {
"particle": "cobblemon:block/functional/habitat_block"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"faces": {
"north": {"uv": [4, 0, 8, 8], "texture": "#particle"},
"east": {"uv": [12, 0, 8, 8], "texture": "#particle"},
"south": {"uv": [12, 0, 16, 8], "texture": "#particle"},
"west": {"uv": [8, 0, 12, 8], "texture": "#particle"},
"up": {"uv": [8, 8, 12, 16], "texture": "#particle"},
"down": {"uv": [12, 8, 16, 16], "texture": "#particle"}
}
},
{
"from": [0.5, 5.5, 0.025],
"to": [3.5, 8.5, 0.025],
"faces": {
"north": {"uv": [0, 0, 0.5, 1], "texture": "#particle"}
}
},
{
"from": [12.5, 5.5, 15.975],
"to": [15.5, 8.5, 15.975],
"faces": {
"south": {"uv": [0, 0, 0.5, 1], "texture": "#particle"}
}
},
{
"from": [0, 10, 0],
"to": [16, 10, 16],
"faces": {
"up": {"uv": [4, 8, 8, 16], "texture": "#particle"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"thirdperson_lefthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"firstperson_righthand": {
"rotation": [0, 45, 0],
"scale": [0.4, 0.4, 0.4]
},
"firstperson_lefthand": {
"rotation": [0, -135, 0],
"scale": [0.4, 0.4, 0.4]
},
"ground": {
"translation": [0, 3, 0],
"scale": [0.25, 0.25, 0.25]
},
"gui": {
"rotation": [30, -135, 0],
"scale": [0.625, 0.625, 0.625]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}
@@ -0,0 +1,43 @@
{
"format_version": "1.21.11",
"credit": "Made with Blockbench",
"texture_size": [64, 32],
"textures": {
"particle": "cobblemon:block/functional/habitat_block"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"faces": {
"north": {"uv": [4, 0, 8, 8], "texture": "#particle"},
"east": {"uv": [12, 0, 8, 8], "texture": "#particle"},
"south": {"uv": [12, 0, 16, 8], "texture": "#particle"},
"west": {"uv": [8, 0, 12, 8], "texture": "#particle"},
"up": {"uv": [8, 8, 12, 16], "texture": "#particle"},
"down": {"uv": [12, 8, 16, 16], "texture": "#particle"}
}
},
{
"from": [0.5, 5.5, 0.025],
"to": [3.5, 8.5, 0.025],
"faces": {
"north": {"uv": [0, 0, 0.5, 1], "texture": "#particle"}
}
},
{
"from": [12.5, 5.5, 15.975],
"to": [15.5, 8.5, 15.975],
"faces": {
"south": {"uv": [0, 0, 0.5, 1], "texture": "#particle"}
}
},
{
"from": [0, 10, 0],
"to": [16, 10, 16],
"faces": {
"up": {"uv": [0, 8, 4, 16], "texture": "#particle"}
}
}
]
}
@@ -0,0 +1,3 @@
{
"parent": "cobblemon:block/habitat_block_natural"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,11 @@
{
"name": "Kanto Route 1",
"type": "cobblemon:natural",
"spawns": [
{ "species": "cobblemon:pidgey", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "2-5", "weight": 12 },
{ "species": "cobblemon:rattata", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "2-5", "weight": 12 },
{ "species": "cobblemon:spearow", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "3-6", "weight": 8 },
{ "species": "cobblemon:caterpie", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "2-4", "weight": 10 },
{ "species": "cobblemon:weedle", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "2-4", "weight": 10 }
]
}
@@ -0,0 +1,36 @@
{
"name": "Kanto Route 1",
"type": "cobblemon:activated",
"spawns": [
{
"species": "cobblemon:pidgey",
"spawnablePositionType": "grounded",
"levelRange": "2-5",
"weight": 12
},
{
"species": "cobblemon:rattata",
"spawnablePositionType": "grounded",
"levelRange": "2-5",
"weight": 12
},
{
"species": "cobblemon:spearow",
"spawnablePositionType": "grounded",
"levelRange": "3-6",
"weight": 8
},
{
"species": "cobblemon:caterpie",
"spawnablePositionType": "grounded",
"levelRange": "2-4",
"weight": 10
},
{
"species": "cobblemon:weedle",
"spawnablePositionType": "grounded",
"levelRange": "2-4",
"weight": 10
}
]
}
@@ -0,0 +1,12 @@
{
"name": "Kanto Route 2",
"type": "cobblemon:natural",
"spawns": [
{ "species": "cobblemon:pidgey", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "3-7", "weight": 12 },
{ "species": "cobblemon:rattata", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "3-7", "weight": 12 },
{ "species": "cobblemon:caterpie", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "3-6", "weight": 10 },
{ "species": "cobblemon:weedle", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "3-6", "weight": 10 },
{ "species": "cobblemon:nidoranf", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "4-8", "weight": 8 },
{ "species": "cobblemon:nidoranm", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "4-8", "weight": 8 }
]
}
@@ -0,0 +1,42 @@
{
"name": "Kanto Route 2",
"type": "cobblemon:activated",
"spawns": [
{
"species": "cobblemon:pidgey",
"spawnablePositionType": "grounded",
"levelRange": "3-7",
"weight": 12
},
{
"species": "cobblemon:rattata",
"spawnablePositionType": "grounded",
"levelRange": "3-7",
"weight": 12
},
{
"species": "cobblemon:caterpie",
"spawnablePositionType": "grounded",
"levelRange": "3-6",
"weight": 10
},
{
"species": "cobblemon:weedle",
"spawnablePositionType": "grounded",
"levelRange": "3-6",
"weight": 10
},
{
"species": "cobblemon:nidoranf",
"spawnablePositionType": "grounded",
"levelRange": "4-8",
"weight": 8
},
{
"species": "cobblemon:nidoranm",
"spawnablePositionType": "grounded",
"levelRange": "4-8",
"weight": 8
}
]
}
@@ -0,0 +1,12 @@
{
"name": "Kanto Route 3",
"type": "cobblemon:natural",
"spawns": [
{ "species": "cobblemon:pidgey", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "6-10", "weight": 10 },
{ "species": "cobblemon:spearow", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "6-11", "weight": 11 },
{ "species": "cobblemon:jigglypuff", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "8-12", "weight": 7 },
{ "species": "cobblemon:nidoranf", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "6-10", "weight": 9 },
{ "species": "cobblemon:nidoranm", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "6-10", "weight": 9 },
{ "species": "cobblemon:mankey", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "8-12", "weight": 6 }
]
}
@@ -0,0 +1,42 @@
{
"name": "Kanto Route 3",
"type": "cobblemon:activated",
"spawns": [
{
"species": "cobblemon:pidgey",
"spawnablePositionType": "grounded",
"levelRange": "6-10",
"weight": 10
},
{
"species": "cobblemon:spearow",
"spawnablePositionType": "grounded",
"levelRange": "6-11",
"weight": 11
},
{
"species": "cobblemon:jigglypuff",
"spawnablePositionType": "grounded",
"levelRange": "8-12",
"weight": 7
},
{
"species": "cobblemon:nidoranf",
"spawnablePositionType": "grounded",
"levelRange": "6-10",
"weight": 9
},
{
"species": "cobblemon:nidoranm",
"spawnablePositionType": "grounded",
"levelRange": "6-10",
"weight": 9
},
{
"species": "cobblemon:mankey",
"spawnablePositionType": "grounded",
"levelRange": "8-12",
"weight": 6
}
]
}
@@ -0,0 +1,11 @@
{
"name": "Kanto Route 4",
"type": "cobblemon:natural",
"spawns": [
{ "species": "cobblemon:rattata", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "7-12", "weight": 11 },
{ "species": "cobblemon:spearow", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "7-12", "weight": 11 },
{ "species": "cobblemon:sandshrew", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "8-14", "weight": 7 },
{ "species": "cobblemon:ekans", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "8-14", "weight": 7 },
{ "species": "cobblemon:mankey", "bucket": "rare", "spawnablePositionType": "grounded", "levelRange": "10-15", "weight": 4 }
]
}
@@ -0,0 +1,36 @@
{
"name": "Kanto Route 4",
"type": "cobblemon:activated",
"spawns": [
{
"species": "cobblemon:rattata",
"spawnablePositionType": "grounded",
"levelRange": "7-12",
"weight": 11
},
{
"species": "cobblemon:spearow",
"spawnablePositionType": "grounded",
"levelRange": "7-12",
"weight": 11
},
{
"species": "cobblemon:sandshrew",
"spawnablePositionType": "grounded",
"levelRange": "8-14",
"weight": 7
},
{
"species": "cobblemon:ekans",
"spawnablePositionType": "grounded",
"levelRange": "8-14",
"weight": 7
},
{
"species": "cobblemon:mankey",
"spawnablePositionType": "grounded",
"levelRange": "10-15",
"weight": 4
}
]
}
@@ -0,0 +1,11 @@
{
"name": "Kanto Route 5",
"type": "cobblemon:natural",
"spawns": [
{ "species": "cobblemon:pidgey", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "10-16", "weight": 10 },
{ "species": "cobblemon:meowth", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "10-16", "weight": 10 },
{ "species": "cobblemon:oddish", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "11-17", "weight": 9 },
{ "species": "cobblemon:bellsprout", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "11-17", "weight": 9 },
{ "species": "cobblemon:pidgeotto", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "15-20", "weight": 6 }
]
}
@@ -0,0 +1,36 @@
{
"name": "Kanto Route 5",
"type": "cobblemon:activated",
"spawns": [
{
"species": "cobblemon:pidgey",
"spawnablePositionType": "grounded",
"levelRange": "10-16",
"weight": 10
},
{
"species": "cobblemon:meowth",
"spawnablePositionType": "grounded",
"levelRange": "10-16",
"weight": 10
},
{
"species": "cobblemon:oddish",
"spawnablePositionType": "grounded",
"levelRange": "11-17",
"weight": 9
},
{
"species": "cobblemon:bellsprout",
"spawnablePositionType": "grounded",
"levelRange": "11-17",
"weight": 9
},
{
"species": "cobblemon:pidgeotto",
"spawnablePositionType": "grounded",
"levelRange": "15-20",
"weight": 6
}
]
}
@@ -0,0 +1,11 @@
{
"name": "Kanto Route 6",
"type": "cobblemon:natural",
"spawns": [
{ "species": "cobblemon:pidgey", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "11-17", "weight": 10 },
{ "species": "cobblemon:meowth", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "11-17", "weight": 10 },
{ "species": "cobblemon:mankey", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "12-18", "weight": 7 },
{ "species": "cobblemon:oddish", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "12-18", "weight": 8 },
{ "species": "cobblemon:bellsprout", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "12-18", "weight": 8 }
]
}
@@ -0,0 +1,36 @@
{
"name": "Kanto Route 6",
"type": "cobblemon:activated",
"spawns": [
{
"species": "cobblemon:pidgey",
"spawnablePositionType": "grounded",
"levelRange": "11-17",
"weight": 10
},
{
"species": "cobblemon:meowth",
"spawnablePositionType": "grounded",
"levelRange": "11-17",
"weight": 10
},
{
"species": "cobblemon:mankey",
"spawnablePositionType": "grounded",
"levelRange": "12-18",
"weight": 7
},
{
"species": "cobblemon:oddish",
"spawnablePositionType": "grounded",
"levelRange": "12-18",
"weight": 8
},
{
"species": "cobblemon:bellsprout",
"spawnablePositionType": "grounded",
"levelRange": "12-18",
"weight": 8
}
]
}
@@ -0,0 +1,12 @@
{
"name": "Kanto Route 7",
"type": "cobblemon:natural",
"spawns": [
{ "species": "cobblemon:pidgey", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "15-22", "weight": 9 },
{ "species": "cobblemon:meowth", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "15-22", "weight": 10 },
{ "species": "cobblemon:vulpix", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "16-24", "weight": 7 },
{ "species": "cobblemon:growlithe", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "16-24", "weight": 7 },
{ "species": "cobblemon:oddish", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "15-22", "weight": 8 },
{ "species": "cobblemon:bellsprout", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "15-22", "weight": 8 }
]
}
@@ -0,0 +1,42 @@
{
"name": "Kanto Route 7",
"type": "cobblemon:activated",
"spawns": [
{
"species": "cobblemon:pidgey",
"spawnablePositionType": "grounded",
"levelRange": "15-22",
"weight": 9
},
{
"species": "cobblemon:meowth",
"spawnablePositionType": "grounded",
"levelRange": "15-22",
"weight": 10
},
{
"species": "cobblemon:vulpix",
"spawnablePositionType": "grounded",
"levelRange": "16-24",
"weight": 7
},
{
"species": "cobblemon:growlithe",
"spawnablePositionType": "grounded",
"levelRange": "16-24",
"weight": 7
},
{
"species": "cobblemon:oddish",
"spawnablePositionType": "grounded",
"levelRange": "15-22",
"weight": 8
},
{
"species": "cobblemon:bellsprout",
"spawnablePositionType": "grounded",
"levelRange": "15-22",
"weight": 8
}
]
}
@@ -0,0 +1,11 @@
{
"name": "Kanto Route 8",
"type": "cobblemon:natural",
"spawns": [
{ "species": "cobblemon:meowth", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "16-24", "weight": 10 },
{ "species": "cobblemon:sandshrew", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "16-24", "weight": 9 },
{ "species": "cobblemon:ekans", "bucket": "common", "spawnablePositionType": "grounded", "levelRange": "16-24", "weight": 9 },
{ "species": "cobblemon:growlithe", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "18-26", "weight": 7 },
{ "species": "cobblemon:vulpix", "bucket": "uncommon", "spawnablePositionType": "grounded", "levelRange": "18-26", "weight": 7 }
]
}
@@ -0,0 +1,36 @@
{
"name": "Kanto Route 8",
"type": "cobblemon:activated",
"spawns": [
{
"species": "cobblemon:meowth",
"spawnablePositionType": "grounded",
"levelRange": "16-24",
"weight": 10
},
{
"species": "cobblemon:sandshrew",
"spawnablePositionType": "grounded",
"levelRange": "16-24",
"weight": 9
},
{
"species": "cobblemon:ekans",
"spawnablePositionType": "grounded",
"levelRange": "16-24",
"weight": 9
},
{
"species": "cobblemon:growlithe",
"spawnablePositionType": "grounded",
"levelRange": "18-26",
"weight": 7
},
{
"species": "cobblemon:vulpix",
"spawnablePositionType": "grounded",
"levelRange": "18-26",
"weight": 7
}
]
}

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