forked from mirrors/cobblemon
Phase 2 of cursed merge, Habitats now only extends 1.8.0 branch
This commit is contained in:
@@ -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
|
||||
}
|
||||
+37
@@ -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
|
||||
}
|
||||
+40
@@ -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")
|
||||
}
|
||||
}
|
||||
+30
@@ -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 }
|
||||
}
|
||||
}
|
||||
+58
@@ -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)
|
||||
}
|
||||
}
|
||||
+101
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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)
|
||||
}
|
||||
}
|
||||
+163
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -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()
|
||||
|
||||
-39
@@ -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
|
||||
}
|
||||
}*/
|
||||
}
|
||||
-30
@@ -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
|
||||
}
|
||||
}
|
||||
-23
@@ -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
|
||||
}
|
||||
-56
@@ -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>()
|
||||
//}
|
||||
+55
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+3
-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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
-80
@@ -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 }
|
||||
}
|
||||
-37
@@ -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?
|
||||
}
|
||||
+33
-7
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
-225
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
-18
@@ -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
|
||||
}
|
||||
+268
@@ -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
|
||||
}
|
||||
+161
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+438
@@ -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
|
||||
}
|
||||
+1
-2
@@ -29,7 +29,7 @@ object DebugKeybindings {
|
||||
TranslateUpKeybinding(),
|
||||
TranslateDownKeybinding(),
|
||||
PrintModelSettingsKeybinding(),
|
||||
ToggleRidingStatsDebugGUIKeybinding()
|
||||
ToggleRidingStatsDebugGUIKeybinding(),
|
||||
)
|
||||
|
||||
class ScaleUpKeybinding: CobblemonKeyBinding(
|
||||
@@ -163,5 +163,4 @@ object DebugKeybindings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+32
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
-37
@@ -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()
|
||||
|
||||
}
|
||||
}
|
||||
+245
@@ -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)
|
||||
|
||||
-3
@@ -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)
|
||||
|
||||
+129
@@ -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)
|
||||
}
|
||||
}
|
||||
-23
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+46
@@ -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)
|
||||
}
|
||||
}
|
||||
-18
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
+48
@@ -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 {
|
||||
|
||||
+9
-6
@@ -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"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+43
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -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"
|
||||
}
|
||||
BIN
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
Reference in New Issue
Block a user