|
| 1 | +package com.typewritermc.basic.entries.variables |
| 2 | + |
| 3 | +import com.typewritermc.core.books.pages.Colors |
| 4 | +import com.typewritermc.core.extension.annotations.Entry |
| 5 | +import com.typewritermc.core.extension.annotations.Min |
| 6 | +import com.typewritermc.core.extension.annotations.Default |
| 7 | +import com.typewritermc.core.extension.annotations.VariableData |
| 8 | +import com.typewritermc.core.interaction.randomSeed |
| 9 | +import com.typewritermc.core.utils.Generic |
| 10 | +import com.typewritermc.engine.paper.entry.entries.ConstVar |
| 11 | +import com.typewritermc.engine.paper.entry.entries.Var |
| 12 | +import com.typewritermc.engine.paper.entry.entries.VarContext |
| 13 | +import com.typewritermc.engine.paper.entry.entries.VariableEntry |
| 14 | +import com.typewritermc.engine.paper.entry.entries.getData |
| 15 | +import java.util.Collections.emptyList |
| 16 | +import kotlin.random.Random |
| 17 | + |
| 18 | +@Entry( |
| 19 | + "weighted_random_variable", |
| 20 | + "A variable that returns a weighted random value of the given values", |
| 21 | + Colors.GREEN, |
| 22 | + "streamline:dices-entertainment-gaming-dices-solid", |
| 23 | +) |
| 24 | +@VariableData(WeightedRandomVariableData::class) |
| 25 | +/** |
| 26 | + * The `WeightedRandomVariableEntry` is a variable that returns one configured value based on weight. |
| 27 | + * |
| 28 | + * ## How could this be used? |
| 29 | + * This can be used for loot rolls, branching dialogue, or any case where outcomes should not have equal chance. |
| 30 | + */ |
| 31 | +class WeightedRandomVariableEntry( |
| 32 | + override val id: String = "", |
| 33 | + override val name: String = "", |
| 34 | + val values: List<WeightedValue> = emptyList(), |
| 35 | +) : VariableEntry { |
| 36 | + override fun <T : Any> get(context: VarContext<T>): T { |
| 37 | + val dataValues = context.getData<WeightedRandomVariableData>()?.values ?: emptyList() |
| 38 | + val allValues = (values + dataValues).filter { it.weight > 0 } |
| 39 | + |
| 40 | + require(allValues.isNotEmpty()) { |
| 41 | + "Weighted random variable '$id' has no values with a positive weight" |
| 42 | + } |
| 43 | + |
| 44 | + val selected = allValues.weightedRandom(Random(context.interactionContext.randomSeed())) |
| 45 | + val selectedValue = selected.value.get(context.player, context.interactionContext) |
| 46 | + val value = selectedValue.get(context.klass) |
| 47 | + ?: throw IllegalStateException( |
| 48 | + "Could not cast generic value ${selectedValue.data} to ${context.klass.qualifiedName}", |
| 49 | + ) |
| 50 | + |
| 51 | + return value |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +data class WeightedRandomVariableData( |
| 56 | + val values: List<WeightedValue> = emptyList(), |
| 57 | +) |
| 58 | + |
| 59 | +data class WeightedValue( |
| 60 | + val value: Var<Generic> = ConstVar(Generic.Empty), |
| 61 | + @Min(1) |
| 62 | + @Default("1") |
| 63 | + val weight: Int = 1, |
| 64 | +) |
| 65 | + |
| 66 | +private fun List<WeightedValue>.weightedRandom(random: Random): WeightedValue { |
| 67 | + require(isNotEmpty()) { |
| 68 | + "Cannot select a weighted random value from an empty list" |
| 69 | + } |
| 70 | + |
| 71 | + if (size == 1) { |
| 72 | + return first() |
| 73 | + } |
| 74 | + |
| 75 | + val cumulativeWeights = scan(0.0) { acc, entry -> acc + entry.weight.toDouble() }.drop(1) |
| 76 | + val sampledWeight = random.nextDouble() * cumulativeWeights.last() |
| 77 | + |
| 78 | + return zip(cumulativeWeights) |
| 79 | + .first { (_, cumulativeWeight) -> cumulativeWeight > sampledWeight } |
| 80 | + .first |
| 81 | +} |
0 commit comments