1+ /*
2+ * Copyright (C) 2025 Accieo
3+ *
4+ * This Source Code Form is subject to the terms of the Mozilla Public
5+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+ */
8+
9+ package accieo.cobbleworkers.jobs
10+
11+ import accieo.cobbleworkers.cache.CobbleworkersCacheManager
12+ import accieo.cobbleworkers.config.CobbleworkersConfigHolder
13+ import accieo.cobbleworkers.enums.JobType
14+ import accieo.cobbleworkers.interfaces.Worker
15+ import accieo.cobbleworkers.utilities.CobbleworkersInventoryUtils
16+ import accieo.cobbleworkers.utilities.CobbleworkersNavigationUtils
17+ import accieo.cobbleworkers.utilities.CobbleworkersTypeUtils
18+ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
19+ import net.minecraft.block.Block
20+ import net.minecraft.block.Blocks
21+ import net.minecraft.item.ItemStack
22+ import net.minecraft.loot.context.LootContextParameterSet
23+ import net.minecraft.loot.context.LootContextParameters
24+ import net.minecraft.loot.context.LootContextTypes
25+ import net.minecraft.registry.RegistryKey
26+ import net.minecraft.registry.RegistryKeys
27+ import net.minecraft.server.world.ServerWorld
28+ import net.minecraft.util.Identifier
29+ import net.minecraft.util.math.BlockPos
30+ import net.minecraft.world.World
31+ import java.util.UUID
32+ import kotlin.text.lowercase
33+
34+ object Archeologist : Worker {
35+ private val config = CobbleworkersConfigHolder .config.archeology
36+ private val cooldownTicks get() = config.archeologyLootingCooldownSeconds * 20L
37+ private val lastGenerationTime = mutableMapOf<UUID , Long >()
38+ private val heldItemsByPokemon = mutableMapOf<UUID , List <ItemStack >>()
39+ private val failedDepositLocations = mutableMapOf<UUID , MutableSet <BlockPos >>()
40+ private val validBlocks: Set <Block > = setOf (
41+ Blocks .DIRT ,
42+ Blocks .GRAVEL ,
43+ Blocks .MUD ,
44+ Blocks .COARSE_DIRT ,
45+ Blocks .ROOTED_DIRT ,
46+ )
47+
48+ override val jobType: JobType = JobType .Archeologist
49+ override val blockValidator: ((World , BlockPos ) -> Boolean ) = { world: World , pos: BlockPos ->
50+ val state = world.getBlockState(pos)
51+ val aboveState = world.getBlockState(pos.up())
52+ state.block in validBlocks && aboveState.isAir
53+ }
54+
55+ /* *
56+ * Determines if Pokémon is eligible to be an archeologist.
57+ * NOTE: This is used to prevent running the tick method unnecessarily.
58+ */
59+ override fun shouldRun (pokemonEntity : PokemonEntity ): Boolean {
60+ if (! config.archeologistsEnabled) return false
61+
62+ return CobbleworkersTypeUtils .isAllowedByType(config.typeDoesArcheology, pokemonEntity) || isDesignatedHarvester(pokemonEntity)
63+ }
64+
65+ /* *
66+ * Main logic loop for the archeologist, executed each tick.
67+ * Delegates to state handlers handleHarvesting and handleDepositing
68+ * to manage the current task of the Pokémon.
69+ *
70+ * NOTE: Origin refers to the pasture's block position.
71+ */
72+ override fun tick (world : World , origin : BlockPos , pokemonEntity : PokemonEntity ) {
73+ val pokemonId = pokemonEntity.pokemon.uuid
74+ val heldItems = heldItemsByPokemon[pokemonId]
75+
76+ if (heldItems.isNullOrEmpty()) {
77+ failedDepositLocations.remove(pokemonId)
78+ handleHarvesting(world, origin, pokemonEntity)
79+ } else {
80+ CobbleworkersInventoryUtils .handleDepositing(world, origin, pokemonEntity, heldItems,
81+ failedDepositLocations,
82+ heldItemsByPokemon
83+ )
84+ }
85+ }
86+
87+ /* *
88+ * Handles logic for archeologist when the Pokémon is not holding items.
89+ */
90+ private fun handleHarvesting (world : World , origin : BlockPos , pokemonEntity : PokemonEntity ) {
91+ val pokemonId = pokemonEntity.pokemon.uuid
92+ val closestBlock = findClosestArcheologySpot(world, origin) ? : return
93+ val currentTarget = CobbleworkersNavigationUtils .getTarget(pokemonId, world)
94+
95+ if (currentTarget == null ) {
96+ if (! CobbleworkersNavigationUtils .isTargeted(closestBlock, world) && ! CobbleworkersNavigationUtils .isRecentlyExpired(closestBlock, world)) {
97+ CobbleworkersNavigationUtils .claimTarget(pokemonId, closestBlock, world)
98+ }
99+ return
100+ }
101+
102+ if (currentTarget == closestBlock) {
103+ CobbleworkersNavigationUtils .navigateTo(pokemonEntity, closestBlock)
104+ }
105+
106+ if (CobbleworkersNavigationUtils .isPokemonAtPosition(pokemonEntity, currentTarget)) {
107+ handleGeneration(world, closestBlock, pokemonEntity)
108+ CobbleworkersNavigationUtils .releaseTarget(pokemonId, world)
109+ }
110+ }
111+
112+ /* *
113+ * Scans the pasture's block surrounding area for the closest archeology spot.
114+ */
115+ private fun findClosestArcheologySpot (world : World , origin : BlockPos ): BlockPos ? {
116+ val possibleTargets = CobbleworkersCacheManager .getTargets(origin, jobType)
117+ if (possibleTargets.isEmpty()) return null
118+
119+ return possibleTargets
120+ .filter { pos ->
121+ blockValidator(world, pos) && ! CobbleworkersNavigationUtils .isRecentlyExpired(pos, world)
122+ }
123+ .minByOrNull { it.getSquaredDistance(origin) }
124+ }
125+
126+ /* *
127+ * Handles logic for generating loot from cobblemon loot table.
128+ */
129+ fun handleGeneration (world : World , origin : BlockPos , pokemonEntity : PokemonEntity ) {
130+ val pokemonId = pokemonEntity.pokemon.uuid
131+ val now = world.time
132+ val lastTime = lastGenerationTime[pokemonId] ? : 0L
133+
134+ if (now - lastTime < cooldownTicks) {
135+ return
136+ }
137+
138+ val lootParams = LootContextParameterSet .Builder (world as ServerWorld )
139+ .add(LootContextParameters .ORIGIN , origin.toCenterPos())
140+ .add(LootContextParameters .THIS_ENTITY , pokemonEntity)
141+ .build(LootContextTypes .CHEST )
142+
143+ val lootTables = config.lootTables.mapNotNull { Identifier .tryParse(it) }
144+
145+ if (lootTables.isEmpty()) return
146+
147+ val selectedId = lootTables.random()
148+
149+ val lootTableKey = RegistryKey .of(RegistryKeys .LOOT_TABLE , selectedId)
150+ val lootTable = world.server.reloadableRegistries.getLootTable(lootTableKey)
151+ val drops = lootTable.generateLoot(lootParams)
152+
153+ if (drops.isNotEmpty()) {
154+ lastGenerationTime[pokemonId] = now
155+ heldItemsByPokemon[pokemonId] = drops
156+ }
157+ }
158+
159+ /* *
160+ * Checks if the Pokémon qualifies as an archeologist because its species is
161+ * explicitly listed in the config.
162+ */
163+ private fun isDesignatedHarvester (pokemonEntity : PokemonEntity ): Boolean {
164+ val speciesName = pokemonEntity.pokemon.species.translatedName.string.lowercase()
165+ return config.archeologists.any { it.lowercase() == speciesName }
166+ }
167+ }
0 commit comments