Skip to content

Commit 9f51b24

Browse files
committed
Deduplicate drop database, fix some bugs, rewrite RationalTable, use Double over Rational for more precision
1 parent f292bae commit 9f51b24

File tree

21 files changed

+26991
-37842
lines changed

21 files changed

+26991
-37842
lines changed

data/game/def/npcs/drops.json

Lines changed: 26884 additions & 37740 deletions
Large diffs are not rendered by default.

src/main/java/io/luna/game/model/item/GroundItem.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,6 @@ public GroundItem(LunaContext context, int id, int amount, Position position, Ch
7575
super(context, position, EntityType.ITEM, view);
7676
checkArgument(ItemDefinition.isIdValid(id), "Invalid item identifier.");
7777
checkArgument(amount > 0, "Amount must be above 0.");
78-
79-
// Non-stackable ground items must be represented one-by-one.
80-
ItemDefinition def = ItemDefinition.ALL.retrieve(id);
81-
checkArgument(def.isStackable() || amount == 1,
82-
"Non-stackable ground items [" + def.getName() + "] have a maximum amount of 1.");
83-
8478
this.id = id;
8579
this.amount = amount;
8680
}

src/main/java/io/luna/util/RandomUtils.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@ public final class RandomUtils {
2828
* @return {@code true} if the roll succeeds, {@code false} otherwise.
2929
*/
3030
public static boolean roll(Rational rational) {
31-
if (rational.getNumerator() <= 0) {
32-
return false;
33-
} else if (rational.getNumerator() >= rational.getDenominator()) {
34-
return true;
35-
} else return ThreadLocalRandom.current().nextLong(0, rational.getDenominator()) < rational.getNumerator();
31+
return roll(rational.doubleValue());
3632
}
3733

3834
/**
@@ -76,7 +72,7 @@ public static double nextDouble() {
7672
* @param value The percent chance to succeed (0–100).
7773
* @return {@code true} if successful, {@code false} otherwise.
7874
*/
79-
public static boolean rollPercent(int value) {
75+
public static boolean roll(int value) {
8076
value = Math.max(0, Math.min(value, 100));
8177
return ThreadLocalRandom.current().nextInt(100) < value;
8278
}
@@ -87,7 +83,7 @@ public static boolean rollPercent(int value) {
8783
* @param value The chance to succeed (0.0–1.0).
8884
* @return {@code true} if successful, {@code false} otherwise.
8985
*/
90-
public static boolean rollPercent(double value) {
86+
public static boolean roll(double value) {
9187
value = Math.max(0.0, Math.min(value, 1.0));
9288
return ThreadLocalRandom.current().nextDouble() <= value;
9389
}

src/main/java/io/luna/util/Rational.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,6 @@ public long getDenominator() {
319319
* Compares this rational to another for ordering.
320320
* <p>
321321
* The comparison is performed using the double-precision value of each number.
322-
* </p>
323322
*
324323
* @param other The other rational to compare against.
325324
* @return A negative value if less, zero if equal, or positive if greater.

src/main/kotlin/api/api/bot/Suspendable.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ object Suspendable {
2626
/**
2727
* Maybes runs [action] based on [prob]. Returns `true` if the action ran.
2828
*/
29-
suspend fun maybe(prob: Rational, action: suspend () -> Unit): Boolean {
29+
suspend fun maybe(prob: Double, action: suspend () -> Unit): Boolean {
3030
if (RandomUtils.roll(prob)) {
3131
action()
3232
return true

src/main/kotlin/api/api/drops/DropTable.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import io.luna.game.model.Entity
55
import io.luna.game.model.item.Item
66
import io.luna.game.model.mob.Mob
77
import io.luna.util.RandomUtils
8-
import io.luna.util.Rational
98

109
/**
1110
* Represents a drop table with a fixed chance to roll for item drops. This is the base class for all drop table types
@@ -19,7 +18,7 @@ import io.luna.util.Rational
1918
*
2019
* @author lare96
2120
*/
22-
abstract class DropTable(private val chance: Rational = ALWAYS) : Iterable<DropTableItem> {
21+
abstract class DropTable(private val chance: Double = ALWAYS) : Iterable<DropTableItem> {
2322

2423
/**
2524
* Rolls on the drop table based on the provided context [mob] (killer) and [source] (victim).
@@ -30,7 +29,7 @@ abstract class DropTable(private val chance: Rational = ALWAYS) : Iterable<DropT
3029
val allItems = mutableListOf<Item>()
3130
if (RandomUtils.roll(chance) && canRollOnTable(mob, source)) {
3231
val items = computeTable(mob, source).filterNot {
33-
val alwaysDrop = it.chance == ALWAYS
32+
val alwaysDrop = it.chance >= 1.0
3433
if (alwaysDrop) {
3534
val alwaysItem = it.toItem()
3635
if (alwaysItem != null) {
@@ -60,7 +59,7 @@ abstract class DropTable(private val chance: Rational = ALWAYS) : Iterable<DropT
6059
if (RandomUtils.roll(item.chance)) item.toItem() else null
6160
}
6261

63-
else -> RationalTable(items.map { it.chance to it }).roll()?.toItem()
62+
else -> ProbabilityTable(items.map { it.chance to it }).roll()?.toItem()
6463
}
6564
}
6665

src/main/kotlin/api/api/drops/DropTableHandler.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ object DropTableHandler {
6969
* @param action The block defining the drop table items.
7070
* @return A simple drop table instance.
7171
*/
72-
fun createSimple(chance: Rational = Rational.ALWAYS, action: DropTableItemReceiver.() -> Unit): SimpleDropTable {
72+
fun createSimple(chance: Double = ALWAYS, action: DropTableItemReceiver.() -> Unit): SimpleDropTable {
7373
return create(action).table { SimpleDropTable(table, chance) }
7474
}
7575

@@ -81,7 +81,7 @@ object DropTableHandler {
8181
* @return A simple drop table with one item.
8282
*/
8383
fun createSingleton(
84-
chance: Rational = Rational.ALWAYS,
84+
chance: Double = ALWAYS,
8585
action: DropTableItemReceiver.() -> DropTableItemChanceReceiver,
8686
): SimpleDropTable {
8787
return createSimple { action(this).chance(chance) }

src/main/kotlin/api/api/drops/DropTableItem.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import io.luna.util.Rational
1313
* @property chance The probability of dropping this item.
1414
* @author lare96
1515
*/
16-
class DropTableItem(val id: Int, val amount: IntRange, val chance: Rational) {
16+
class DropTableItem(val id: Int, val amount: IntRange, val chance: Double) {
1717

1818
companion object {
1919

@@ -38,13 +38,13 @@ class DropTableItem(val id: Int, val amount: IntRange, val chance: Rational) {
3838
}
3939

4040

41-
constructor(name: String, amount: IntRange, chance: Rational, noted: Boolean = false) :
41+
constructor(name: String, amount: IntRange, chance: Double, noted: Boolean = false) :
4242
this(computeId(name, noted), amount, chance)
4343

44-
constructor(name: String, amount: Int, chance: Rational, noted: Boolean = false) :
44+
constructor(name: String, amount: Int, chance: Double, noted: Boolean = false) :
4545
this(computeId(name, noted), amount..amount, chance)
4646

47-
constructor(id: Int, amount: Int, chance: Rational) :
47+
constructor(id: Int, amount: Int, chance: Double) :
4848
this(id, amount..amount, chance)
4949

5050
override fun toString(): String {

src/main/kotlin/api/api/drops/GenericDropTables.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ object GenericDropTables {
9393
* @param chance Chance to roll on this table.
9494
* @param rowBonus Enables RoW effects programmatically.
9595
*/
96-
fun gemDropTable(chance: Rational = ALWAYS, rowBonus: Boolean = false): DropTable {
96+
fun gemDropTable(chance: Double = ALWAYS, rowBonus: Boolean = false): DropTable {
9797
return DropTableHandler.create {
9898
nothing(1 of 2)
9999
"Uncut sapphire" x 1 chance (1 of 4)
@@ -136,7 +136,7 @@ object GenericDropTables {
136136
* @param chance Chance to roll on this table.
137137
* @param rowBonus Enables RoW effects programmatically.
138138
*/
139-
fun rareDropTable(chance: Rational = ALWAYS, rowBonus: Boolean = false): DropTable {
139+
fun rareDropTable(chance: Double = ALWAYS, rowBonus: Boolean = false): DropTable {
140140
return DropTableHandler.create {
141141
"Nature rune" x 40..70 chance (1 of 42)
142142
"Adamant javelin" x 10..20 chance (1 of 64)
@@ -181,7 +181,7 @@ object GenericDropTables {
181181
* @param chance Chance to roll on this table.
182182
* @param rowBonus Enables RoW effects programmatically.
183183
*/
184-
fun megaRareDropTable(chance: Rational = ALWAYS, rowBonus: Boolean = false): DropTable {
184+
fun megaRareDropTable(chance: Double = ALWAYS, rowBonus: Boolean = false): DropTable {
185185
return DropTableHandler.create {
186186
nothing(22 of 25)
187187
"Rune spear" x 1..5 chance (1 of 16)
@@ -208,7 +208,7 @@ object GenericDropTables {
208208
* @param combatLevelFactor If true, seed tier is scaled by the mob's combat level.
209209
* @param chance Chance to roll on this table.
210210
*/
211-
fun generalSeedDropTable(combatLevelFactor: Boolean = true, chance: Rational = ALWAYS): DropTable {
211+
fun generalSeedDropTable(combatLevelFactor: Boolean = true, chance: Double = ALWAYS): DropTable {
212212
return DropTableHandler.create {}.table {
213213
object : DropTable(chance) {
214214
override fun computeTable(mob: Mob?, source: Entity?): DropTableItemList {
@@ -247,7 +247,7 @@ object GenericDropTables {
247247
*
248248
* @param chance Chance to roll on this table.
249249
*/
250-
fun rareSeedDropTable(chance: Rational = ALWAYS): SimpleDropTable {
250+
fun rareSeedDropTable(chance: Double = ALWAYS): SimpleDropTable {
251251
return DropTableHandler.createSimple(chance) {
252252
"Toadflax seed" x 1 chance (1 of 5)
253253
"Irit seed" x 1 chance (1 of 7)
@@ -269,7 +269,7 @@ object GenericDropTables {
269269
*
270270
* @param chance Chance to roll on this table.
271271
*/
272-
fun treeHerbSeedDropTable(chance: Rational): SimpleDropTable {
272+
fun treeHerbSeedDropTable(chance: Double): SimpleDropTable {
273273
return DropTableHandler.createSimple(chance) {
274274
"Ranarr seed" x 1 chance (1 of 8)
275275
"Snapdragon seed" x 1 chance (1 of 8)
@@ -292,7 +292,7 @@ object GenericDropTables {
292292
*
293293
* @param chance Chance to roll on this table.
294294
*/
295-
fun uncommonSeedDropTable(chance: Rational = ALWAYS): DropTable {
295+
fun uncommonSeedDropTable(chance: Double = ALWAYS): DropTable {
296296
return DropTableHandler.createSimple(chance) {
297297
items += generalSeedDropList3
298298
items += generalSeedDropList4
@@ -307,7 +307,7 @@ object GenericDropTables {
307307
*
308308
* @param chance Chance to roll on this table.
309309
*/
310-
fun usefulHerbDropTable(chance: Rational = ALWAYS): DropTable {
310+
fun usefulHerbDropTable(chance: Double = ALWAYS): DropTable {
311311
return DropTableHandler.createSimple(chance) {
312312
noted {
313313
"Grimy avantoe" x 1..3 chance (1 of 3)
@@ -323,7 +323,7 @@ object GenericDropTables {
323323
*
324324
* @param chance Chance to roll on this table.
325325
*/
326-
fun herbDropTable(chance: Rational = ALWAYS): DropTable {
326+
fun herbDropTable(chance: Double = ALWAYS): DropTable {
327327
return DropTableHandler.createSimple(chance) {
328328
"Grimy guam leaf" x 1 chance (1 of 4)
329329
"Grimy marrentill" x 1 chance (1 of 5)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package api.drops
2+
3+
import api.predef.*
4+
5+
/**
6+
* A table that selects one outcome from a list of probability entries.
7+
*
8+
* Each entry consists of a probability value and its associated result.
9+
*
10+
* When the total chance is less than or equal to `1.0`, the table behaves as an absolute probability table.
11+
* Any unclaimed probability space results in no selection being made.
12+
*
13+
* When the total chance exceeds `1.0`, the table falls back to relative-weight selection. In that mode, entry
14+
* values are treated as weights rather than absolute probabilities.
15+
*
16+
* @param entries The probability entries to roll against.
17+
*
18+
* @author lare96
19+
*/
20+
class ProbabilityTable<T>(private val entries: List<Pair<Double, T>>) {
21+
22+
/**
23+
* Rolls this table and returns the selected value, if any.
24+
*
25+
* If the combined chance of all entries is less than or equal to `1.0`, this performs an absolute
26+
* probability roll over the range `[0.0, 1.0]`. If the roll lands outside all entry ranges, this returns
27+
* `null`.
28+
*
29+
* If the combined chance exceeds `1.0`, this performs a relative-weight roll over the range
30+
* `[0.0, totalChance]` instead. This preserves selection behavior for oversized tables, but the entry values
31+
* are no longer interpreted as absolute probabilities.
32+
*
33+
* @return The selected value, or `null` if no value is selected.
34+
*/
35+
fun roll(): T? {
36+
if (entries.isEmpty()) {
37+
// No table to roll on.
38+
return null
39+
}
40+
41+
val totalChance = entries.sumOf { it.first }
42+
if (totalChance > 1.0) {
43+
logger.warn(
44+
"Absolute selection is invalid when total chance exceeds 1.0. Falling back to relative mode instead."
45+
)
46+
}
47+
48+
val roll = if (totalChance > 1.0) rand().nextDouble(totalChance) else rand().nextDouble()
49+
var current = 0.0
50+
51+
for ((chance, value) in entries) {
52+
// Advance the cumulative boundary until the rolled point is reached.
53+
current += chance
54+
if (roll < current) {
55+
return value
56+
}
57+
}
58+
59+
// The roll landed in unclaimed probability space.
60+
return null
61+
}
62+
}

0 commit comments

Comments
 (0)