Added more molang expressions to a bunch of composed pool properties, fixed some bugs

This commit is contained in:
Paul
2026-02-14 00:35:17 -05:00
committed by gruxor
parent e40a3331e2
commit af491f02a2
14 changed files with 232 additions and 109 deletions
@@ -9,14 +9,18 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.npc.PartyPool
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.IntRangeAdapter
import com.cobblemon.mod.common.util.adapters.MovesetBuilderReferenceAdapter
import com.cobblemon.mod.common.util.adapters.PokemonPropertiesAdapter
import com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
@@ -35,7 +39,9 @@ object CobblemonPartyPools : JsonDataRegistry<PartyPool> {
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(PokemonProperties::class.java, PokemonPropertiesAdapter(saveLong = false))
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapter(ScriptableIntRange::class.java, ScriptableIntRangeAdapter)
.registerTypeAdapter(MovesetBuilder::class.java, MovesetBuilderReferenceAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.setPrettyPrinting()
.create()
@@ -15,6 +15,7 @@ import com.cobblemon.mod.common.api.npc.partyproviders.SimplePartyProvider
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.google.gson.JsonElement
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
/**
@@ -26,15 +27,15 @@ import net.minecraft.server.level.ServerPlayer
*/
interface NPCPartyProvider {
companion object {
val types = mutableMapOf<String, (String) -> NPCPartyProvider>(
SimplePartyProvider.TYPE to { SimplePartyProvider() },
PoolPartyProvider.TYPE to { PoolPartyProvider() },
ScriptPartyProvider.TYPE to { ScriptPartyProvider() },
ComposedPoolPartyProvider.TYPE to { ComposedPoolPartyProvider() }
val types = mutableMapOf(
SimplePartyProvider.TYPE to ::SimplePartyProvider,
PoolPartyProvider.TYPE to ::PoolPartyProvider,
ScriptPartyProvider.TYPE to ::ScriptPartyProvider,
ComposedPoolPartyProvider.TYPE to ::ComposedPoolPartyProvider
)
}
val type: String
val type: ResourceLocation
val isStatic: Boolean
fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer> = emptyList()): NPCPartyStore
// Why did I opt for manual JSON loading??? I must have had a reason but I can't remember. Maybe for S2C? Use a codec doofus
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.cobblemonResource
import kotlin.random.Random
@@ -29,12 +30,12 @@ class PartyComposition {
val slot5 = listOf<String>()
val slot6 = listOf<String>()
fun compose(pool: PartyPool, level: Int, aspects: Set<String>, desiredPokemonCount: Int, random: Random = Random.Default): List<Pokemon> {
fun compose(pool: PartyPool, level: Int, aspects: Set<String>, desiredPokemonCount: Int, runtime: MoLangRuntime, random: Random = Random.Default): List<Pokemon> {
val chosenEntries = mutableListOf<PartyPool.PoolEntry>()
val availableEntries = pool.entries.filter { level in it.npcLevels && (it.npcAspects.isEmpty() || it.npcAspects.all(aspects::contains)) }
val availableEntries = pool.entries.filter { level in it.npcLevels.resolve(runtime) && (it.npcAspects.isEmpty() || it.npcAspects.all(aspects::contains)) }
for (labelSet in listOf(slot1, slot2, slot3, slot4, slot5, slot6)) {
pool.tryChoosingEntry(labelSet, random, availableEntries, chosenEntries)
pool.tryChoosingEntry(labelSet, runtime, random, availableEntries, chosenEntries)
if (chosenEntries.size >= desiredPokemonCount) {
break
}
@@ -47,10 +48,11 @@ class PartyComposition {
}
val pokemon = finalEntries.map { entry ->
val lvlVariation = if (entry.levelVariation.first == entry.levelVariation.last) {
entry.levelVariation.first
val levelVariation = entry.levelVariation.resolve(runtime)
val lvlVariation = if (levelVariation.first == levelVariation.last) {
levelVariation.first
} else {
entry.levelVariation.random(random)
levelVariation.random(random)
}
val finalLevel = level + lvlVariation
@@ -8,9 +8,14 @@
package com.cobblemon.mod.common.api.npc
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.moves.MovesetBuilder
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.util.ScriptableIntRange
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.toProperties
import com.cobblemon.mod.common.util.weightedSelection
import kotlin.random.Random
@@ -26,18 +31,18 @@ class PartyPool {
val displayName = "cobblemon.party_pool.$id"
val entries = mutableListOf<PoolEntry>()
class PoolEntry(
val pokemon: PokemonProperties,
val labels: List<String> = emptyList(),
val npcAspects: List<String> = emptyList(),
val required: List<String> = emptyList(),
val excluded: List<String> = emptyList(),
val maxSelectableTimes: Int = 6,
val levelVariation: IntRange = 0..0,
val npcLevels: IntRange = 1..100,
val movesetBuilders: List<MovesetBuilder> = emptyList(),
val weight: Int = 50
)
class PoolEntry {
val pokemon: PokemonProperties = "pikachu".toProperties()
val labels: List<String> = emptyList()
val npcAspects: List<String> = emptyList()
val required: List<String> = emptyList()
val excluded: List<String> = emptyList()
val maxSelectableTimes: ExpressionLike = "6".asExpressionLike()
val levelVariation: ScriptableIntRange = ScriptableIntRange(0, 0)
val npcLevels: ScriptableIntRange = ScriptableIntRange(1, 100)
val movesetBuilders: List<MovesetBuilder> = emptyList()
val weight: ExpressionLike = "50".asExpressionLike()
}
/**
* Goes through each of the labels in order, trying to find a valid choice. Once it finds one, it skips the later
@@ -45,6 +50,7 @@ class PartyPool {
*/
fun tryChoosingEntry(
labels: List<String>,
runtime: MoLangRuntime,
random: Random,
availableEntries: List<PoolEntry>,
chosenEntries: MutableList<PoolEntry>,
@@ -58,11 +64,11 @@ class PartyPool {
// Check excluded labels
entry.excluded.none { it in labels } &&
// Check max selectable times
chosenEntries.count { it == entry } < entry.maxSelectableTimes
chosenEntries.count { it == entry } < entry.maxSelectableTimes.resolveFloat(runtime)
}
if (entries.isNotEmpty()) {
val selectedEntry = entries.weightedSelection(random) { it.weight } ?: return
val selectedEntry = entries.weightedSelection(random) { it.weight.resolveFloat(runtime) } ?: return
chosenEntries.add(selectedEntry)
return
}
@@ -23,6 +23,7 @@ import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.withQueryValue
import com.google.gson.JsonElement
@@ -41,10 +42,10 @@ import net.minecraft.server.level.ServerPlayer
*/
class ComposedPoolPartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "composed_pool"
val TYPE = cobblemonResource("composed_pool")
}
override val type: String = TYPE
override val type = TYPE
override var isStatic: Boolean = true
var useFixedRandom: Boolean = false
var minPokemon: Expression = "1".asExpression()
@@ -82,6 +83,7 @@ class ComposedPoolPartyProvider : NPCPartyProvider {
level = level,
aspects = npc.aspects,
desiredPokemonCount = desiredPokemonCount,
runtime = runtime,
random = random
)
@@ -48,7 +48,7 @@ import kotlin.random.Random
*/
class PoolPartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "pool"
val TYPE = cobblemonResource("pool")
}
@Transient
@@ -28,7 +28,7 @@ import net.minecraft.server.level.ServerPlayer
class ScriptPartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "script"
val TYPE = cobblemonResource("script")
}
override val type = TYPE
@@ -16,6 +16,7 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.getBooleanOrNull
import com.cobblemon.mod.common.util.toProperties
import com.google.gson.JsonElement
@@ -29,7 +30,7 @@ import net.minecraft.server.level.ServerPlayer
*/
class SimplePartyProvider : NPCPartyProvider {
companion object {
const val TYPE = "simple"
val TYPE = cobblemonResource("simple")
}
@Transient
@@ -82,14 +82,14 @@ object CobblemonDataProvider : DataProvider {
this.register(ActionEffects, reloadable = true)
this.register(Moves, reloadable = false)
this.register(CobblemonMovesetBuilders, reloadable = false)
this.register(CobblemonPartyPools, reloadable = false)
this.register(CobblemonPartyCompositions, reloadable = false)
this.register(Abilities, reloadable = false)
this.register(CobblemonBehaviours, reloadable = false)
this.register(PokemonSpecies, reloadable = false)
this.register(SpeciesAdditions, reloadable = false)
this.register(PokeBalls, reloadable = false)
this.register(PropertiesCompletionProvider, reloadable = false)
this.register(CobblemonPartyPools, reloadable = false)
this.register(CobblemonPartyCompositions, reloadable = false)
this.register(SpawnDetailPresets, reloadable = true)
this.register(CobblemonSpawnRules, reloadable = true)
this.register(CobblemonMechanics, reloadable = true)
@@ -191,7 +191,7 @@ class EmptyPokeBallEntity : ThrowableItemProjectile, PosableEntity, WaterDragMod
}
}
} else {
setNoGravity(false)
isNoGravity = false
deltaMovement = Vec3.ZERO
}
}
@@ -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.util
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molang.ExpressionLike
/**
* An IntRange where the minimum and maximum values are defined by MoLang expressions. When deserializing from
* JSON the [com.cobblemon.mod.common.util.adapters.ScriptableIntRangeAdapter] will figure out if it's a single
* value in which case it's not using any scripting stuff. Otherwise, if it's an object with "min" and "max" properties
* it will respect them as expressions.
*
* @author Hiroku
* @since December 15th, 2025
*/
class ScriptableIntRange(
val min: ExpressionLike = "0".asExpressionLike(),
val max: ExpressionLike = "0".asExpressionLike()
) {
constructor(min: Int, max: Int): this(
min = min.toString().asExpressionLike(),
max = max.toString().asExpressionLike()
)
fun resolve(runtime: MoLangRuntime): IntRange {
val minimum = min.resolveInt(runtime)
val maximum = max.resolveInt(runtime)
if (minimum > maximum) {
Cobblemon.LOGGER.error("ScriptableIntRange(${min.getString()}, ${max.getString()}) resolved a minimum ($minimum) greater than maximum ($maximum). Returning empty range.")
return IntRange.EMPTY
}
return minimum..maximum
}
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.util.adapters
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
@@ -16,8 +17,8 @@ import java.lang.reflect.Type
object NPCPartyProviderAdapter : JsonDeserializer<NPCPartyProvider> {
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): NPCPartyProvider {
val typeName = if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString
return NPCPartyProvider.types[typeName]?.invoke(typeName)?.also { it.loadFromJSON(json) }
val typeName = (if (json.isJsonPrimitive) json.asString else json.asJsonObject.get("type").asString).asIdentifierDefaultingNamespace()
return NPCPartyProvider.types[typeName]?.invoke()?.also { it.loadFromJSON(json) }
?: throw IllegalStateException("Unable to find party provider by type: $typeName")
}
}
@@ -0,0 +1,53 @@
/*
* 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.util.ScriptableIntRange
import com.cobblemon.mod.common.util.asExpressionLike
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import java.lang.reflect.Type
/**
* Parses a [ScriptableIntRange] by two paths. One is if it's a primitive in which
* case they're just doing a normal [IntRange] and it will parse it using the same
* method as IntRange normally gets and then returns a ScriptableIntRange of literals.
*
* The other path is if they provide an object with "min" and "max" in which case
* each property is deserialized as an [ExpressionLike].
*
* @author Hiroku
* @since December 15th, 2025
*/
object ScriptableIntRangeAdapter : JsonDeserializer<ScriptableIntRange> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): ScriptableIntRange {
if (json is JsonPrimitive) {
IntRangeAdapter.deserialize(json, typeOfT, context).let {
return ScriptableIntRange(
it.first.toString().asExpressionLike(),
it.last.toString().asExpressionLike()
)
}
} else {
val obj = json.asJsonObject
val min = obj.get("min").asString
val max = obj.get("max").asString
return ScriptableIntRange(
min.toString().asExpressionLike(),
max.toString().asExpressionLike()
)
}
}
}
+82 -73
View File
@@ -48,75 +48,94 @@ For example, take `alpha.json`, used for alpha Pokémon:
This moveset builder will try to fill slot 1 with the last suitable offensive move the Pokémon can learn,
falling back to the last offensive move, and then the last level-up move if all else fails.
### Molang Expressions in Composed Parties
For the Molang expression properties in the following sections, the following Molang queries will be available:
`q.players` - A list of players that are challenging the NPC.
`q.player` - The player challenging the NPC, specifically if being challenged only by one player. If there are multiple players, this will be 0.
`q.npc` - The NPC entity.
`q.level` - The seed level of the NPC.
### Party Pools
The `party_pools` datapack folder has all the pools of Pokémon that can be used to draw from when composing a party.
Each entry of the pool has some number of labels used by the party composition.
| Property | Description |
|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `pokemon` | The guaranteed Pokémon information, such as `geodude shiny=true` |
| `labels` | The identifying labels of the Pokémon. These can be anything you want. Definitely shouldn't be empty though. |
| `npcLevels` | The seed levels that the NPC can be for which this entry is possible. 1-30 would mean a level 40 NPC cannot use that entry. |
| `npcAspects` | The aspects that the NPC must have for this entry to be possible. If the list is empty, as is the default, then it won't check the NPC aspects. |
| `weight` | The relative weight of this entry compared to others in the pool. Defaults to 50. |
| `levelVariation` | The possible variation around the NPC's seed level that is possible. `-2-3` would mean the Pokémon's level could be anywhere from 2 levels below the NPC seed level to 3 levels above. |
| `maxTimesSelectable` | The maximum number of times this entry can be selected for a party. Defaults to 6. |
| `movesetBuilders` | A list of moveset builders from the `moveset_builders` folder that can be used to create this Pokémon's moveset. One will be selected at random if there are multiple. |
| `requires` | A list of labels that must be present in the party for this entry to be selected. These are from the `labels` of entries that were already selected. |
| `incompatible` | A list of labels that cannot be present in the party for this entry to be selected. These are from the `labels` of entries that were already selected. |
| Property | Description |
|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `pokemon` | The guaranteed Pokémon information, such as `geodude shiny=true` |
| `labels` | The identifying labels of the Pokémon. These can be anything you want. Definitely shouldn't be empty though. |
| `npcLevels` | The seed levels that the NPC can be for which this entry is possible. 1-30 would mean a level 40 NPC cannot use that entry. You can also provide an object with `min` and `max` properties to use MoLang expressions. |
| `npcAspects` | The aspects that the NPC must have for this entry to be possible. If the list is empty, as is the default, then it won't check the NPC aspects. |
| `weight` | The relative weight of this entry compared to others in the pool. Defaults to 50. You can also use a Molang expression here. |
| `levelVariation` | The possible variation around the NPC's seed level that is possible. `-2-3` would mean the Pokémon's level could be anywhere from 2 levels below the NPC seed level to 3 levels above. You can also provide an object with `min` and `max` properties to use MoLang expressions. |
| `maxTimesSelectable` | The maximum number of times this entry can be selected for a party. Defaults to 6. You can also use a Molang expression here. |
| `movesetBuilders` | A list of moveset builders from the `moveset_builders` folder that can be used to create this Pokémon's moveset. One will be selected at random if there are multiple. |
| `requires` | A list of labels that must be present in the party for this entry to be selected. These are from the `labels` of entries that were already selected. |
| `incompatible` | A list of labels that cannot be present in the party for this entry to be selected. These are from the `labels` of entries that were already selected. |
As an example, a trivially small pool might look like:
```json
[
{
"pokemon": "geodude",
"labels": [
"basic",
"rocky"
],
"npcLevels": "1-30",
"weight": 20,
"levelVariation": "-1-3",
"maxTimesSelectable": 4,
"movesetBuilders": ["cobblemon:npc_pokemon"],
"requires": [
"golem-ace"
],
"incompatible": [
"steelix-ace"
]
},
{
"pokemon": "golem",
"labels": [
"ace",
"strong"
],
"npcLevels": "1-30",
"weight": 20,
"levelVariation": "1-5",
"maxTimesSelectable": 1,
"movesetBuilders": ["cobblemon:offensive_npc_pokemon", "cobblemon:npc_pokemon"],
"incompatible": [
"steelix-ace",
"ace"
]
},
{
"pokemon": "steelix shiny=true",
"labels": [
"ace",
"strong"
],
"npcLevels": "15-30",
"npcAspects": ["steel-boots"],
"weight": 10,
"levelVariation": "0-2",
"maxTimesSelectable": 1,
"movesetBuilders": ["cobblemon:offensive_npc_pokemon"],
"requires": []
}
]
{
"displayName": "Some Name",
"entries": [
{
"pokemon": "geodude",
"labels": [
"basic",
"rocky"
],
"npcLevels": "1-30",
"weight": 20,
"levelVariation": {
"min": "q.level < 10 ? -1 : -3",
"max": "q.level < 10 ? 1 : 3"
},
"maxTimesSelectable": 4,
"movesetBuilders": ["cobblemon:npc_pokemon"],
"requires": [
"golem-ace"
],
"incompatible": [
"steelix-ace"
]
},
{
"pokemon": "golem",
"labels": [
"ace",
"strong"
],
"npcLevels": "1-30",
"weight": 20,
"levelVariation": "1-5",
"maxTimesSelectable": 1,
"movesetBuilders": ["cobblemon:offensive_npc_pokemon", "cobblemon:npc_pokemon"],
"incompatible": [
"steelix-ace",
"ace"
]
},
{
"pokemon": "steelix shiny=true",
"labels": [
"ace",
"strong"
],
"npcLevels": "15-30",
"npcAspects": ["steel-boots"],
"weight": "q.level < 20 ? 10 : 30",
"levelVariation": "0-2",
"maxTimesSelectable": "q.level > 25 ? 2 : 1",
"movesetBuilders": ["cobblemon:offensive_npc_pokemon"],
"requires": [],
"incompatible": []
}
]
}
```
### Party Compositions
@@ -150,16 +169,6 @@ The `composed_pool` party provider type uses a party pool and a party compositio
party. When generating the party, it first decides how many Pokémon to select, based on the `minPokemon` and `maxPokemon`
expressions. It then uses the party composition and pool to select that many Pokémon and builds a party.
For the Molang expression properties, the following Molang queries will be available:
`q.players` - A list of players that are challenging the NPC.
`q.player` - The player challenging the NPC, specifically if being challenged only by one player. If there are multiple players, this will be 0.
`q.npc` - The NPC entity.
`q.level` - The seed level of the NPC.
| Property | Description |
|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -174,7 +183,7 @@ An example `composed_pool` party provider might look like:
```json
{
"type": "cobblemon:composed_pool",
"type": "composed_pool",
"isStatic": false,
"useFixedRandom": true,
"minPokemon": "q.level > 30 ? 3 : 1",