Skip to content

Commit

Permalink
Complete drop table system with specialized DSL. Just needs NPC drop …
Browse files Browse the repository at this point in the history
…registration (will be done with combat)
  • Loading branch information
lare96 committed Dec 30, 2024
1 parent 5303019 commit 708a428
Show file tree
Hide file tree
Showing 13 changed files with 904 additions and 0 deletions.
77 changes: 77 additions & 0 deletions src/main/kotlin/api/item/dropTable/DropTable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package api.item.dropTable

import api.item.dropTable.DropTableHandler.rollSuccess
import api.predef.*
import io.luna.game.model.Entity
import io.luna.game.model.item.Item
import io.luna.game.model.mob.Mob
import io.luna.util.RandomUtils
import io.luna.util.Rational

/**
* Represents a standard drop table. All drop tables must maintain an immutable internal state to ensure
* instances can be reused.
*
* @author lare96
*/
abstract class DropTable(private val chance: Rational = ALWAYS) : Iterable<DropTableItem> {

/**
* Rolls on this drop table and returns the selected items.
*/
fun roll(mob: Mob?, source: Entity?): MutableList<Item> {
val allItems = mutableListOf<Item>()
if (canRollOnTable(mob, source)) {
val items = computeTable(mob, source).filterNot {
val alwaysDrop = it.chance == ALWAYS
if (alwaysDrop) {
val alwaysItem = it.toItem()
if (alwaysItem != null) {
allItems += alwaysItem
}
}
alwaysDrop
}
val pickedItem = rollOnTable(mob, source, items)
if (pickedItem != null) {
allItems += pickedItem
}
return allItems
}
return allItems
}

/**
* Rolls on the table items by building a rational table with empty slots, and returns the rolled on item.
*/
private fun rollOnTable(mob: Mob?, source: Entity?, items: DropTableItemList): Item? {
if(items.isEmpty()) {
return null
} else if (items.size == 1) {
// Only one item, so our rational table is just the chance that the single item is dropped.
val pickedItem = items.first()
return if (rollSuccess(pickedItem)) pickedItem.toItem() else null
} else {
return RationalTable(items.map { it.chance to it }).roll()?.toItem()
}
}

/**
* Determines if this table can be rolled on. If `false`, `null` will always be returned from [roll].
*/
open fun canRollOnTable(mob: Mob?, source: Entity?): Boolean {
return RandomUtils.rollSuccess(chance)
}

/**
* Dynamically computes the table of items that will be rolled on.
*/
abstract fun computeTable(mob: Mob?, source: Entity?): DropTableItemList

/**
* Returns all possible items that can be rolled on.
*/
abstract fun computePossibleItems(): DropTableItemList

override fun iterator(): Iterator<DropTableItem> = computePossibleItems().iterator()
}
113 changes: 113 additions & 0 deletions src/main/kotlin/api/item/dropTable/DropTableHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package api.item.dropTable

import api.item.dropTable.dsl.DropTableItemChanceReceiver
import api.item.dropTable.dsl.DropTableItemReceiver
import api.item.dropTable.dsl.NpcDropSetReceiver
import api.item.dropTable.dsl.SpecializedTableReceiver
import api.predef.*
import io.luna.game.model.mob.Mob
import io.luna.game.model.mob.Npc
import io.luna.util.RandomUtils
import io.luna.util.Rational

/**
* A type alias for lists of [DropTableItem] types.
*/
typealias DropTableItemList = List<DropTableItem>

/**
* Handles global functions related to drop tables.
*
* @author lare96
*/
object DropTableHandler {

/**
* Represents a single conditional drop.
*/
internal class ConditionalDrop(val condFunc: (Mob?, Npc) -> Boolean, val tableSet: NpcDropTableSet)

/**
* The conditional drop list.
*/
private val conditionalDropList = arrayListOf<ConditionalDrop>()

/**
* The NPC id -> NpcDropTableSet map.
*/
private val npcIdDropMap = hashMapOf<Int, NpcDropTableSet>()

/**
* Determines if [tableItem] will be picked based on its rarity.
*/
internal fun rollSuccess(tableItem: DropTableItem): Boolean = RandomUtils.rollSuccess(tableItem.chance)

/**
* Creates a generic [DropTable] using our specialized DSL.
*/
fun create(action: DropTableItemReceiver.() -> Unit): SpecializedTableReceiver {
// Build the table.
val items = arrayListOf<DropTableItem>()
val builder = DropTableItemReceiver(items, false)
action(builder)
return SpecializedTableReceiver(builder)
}

/**
* Creates a new [SimpleDropTable] instance using our specialized DSL.
*/
fun createSimple(chance: Rational = Rational.ALWAYS, action: DropTableItemReceiver.() -> Unit): SimpleDropTable {
return create(action).table { SimpleDropTable(items, chance) }
}

/**
* Creates a new [SimpleDropTable] with a single item.
*/
fun createSingleton(
chance: Rational = Rational.ALWAYS,
action: DropTableItemReceiver.() -> DropTableItemChanceReceiver,
): SimpleDropTable {
return createSimple { action(this).chance(chance) }
}

/**
* Creates a new [List] of [DropTableItem] instances using our specialized DSL.
*/
fun createList(action: DropTableItemReceiver.() -> Unit): DropTableItemList {
// Build the table.
val items = arrayListOf<DropTableItem>()
action(DropTableItemReceiver(items, false))
return items
}

/**
* Creates a new [NpcDropTableSet] instance using our specialized DSL.
*/
fun createNpcSet(action: NpcDropSetReceiver.() -> Unit): NpcDropTableSet {
// Build the tables.
val tables = arrayListOf<DropTable>()
action(NpcDropSetReceiver(tables))
return NpcDropTableSet(tables)
}

/**
* Registers a [NpcDropTableSet] to trigger on [condFunc] when an [Npc] dies.
*/
fun register(condFunc: (Mob?, Npc) -> Boolean, tableSet: NpcDropTableSet) =
conditionalDropList.add(ConditionalDrop(condFunc, tableSet))

/**
* Registers a [NpcDropTableSet] to trigger when an [Npc] with [id] dies.
*/
fun register(id: Int, tableSet: NpcDropTableSet) {
check(npcIdDropMap.putIfAbsent(id, tableSet) != null)
{ "NPC with id[$id] already has a registered NpcDropTableSet" }
}

/**
* Combines [tables] into one standard [MergedDropTable].
*/
fun createMerged(vararg tables: DropTable): DropTable {
return MergedDropTable(tables.toList())
}
}
77 changes: 77 additions & 0 deletions src/main/kotlin/api/item/dropTable/DropTableItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package api.item.dropTable

import api.predef.*
import com.google.common.base.MoreObjects
import io.luna.game.model.def.ItemDefinition
import io.luna.game.model.item.Item
import io.luna.util.Rational

/**
* A model representing an item within a [NpcDropTableSet]. Please keep in mind that [amount] works inclusively both ways.
*
* @author lare96
*/
class DropTableItem(val id: Int, val amount: IntRange, val chance: Rational) {

companion object {

/**
* Priority name -> ID mappings to avoid conflicts.
*/
private val PRIORITY = mapOf(
"Coins" to 995
)

/**
* Computes the [id] for an item with [name].
*/
fun computeId(name: String, noted: Boolean): Int {
val priorityValue = PRIORITY[name]
if (priorityValue != null) {
return priorityValue
}
return ItemDefinition.ALL.lookup { it.name == name && it.isNoted == noted }
.orElseThrow { NoSuchElementException("Item with name [$name] not found.") }.id
}
}

/**
* A constructor that takes [name] instead of [id].
*/
constructor(name: String, amount: IntRange, chance: Rational, noted: Boolean = false) :
this(computeId(name, noted), amount, chance)

/**
* A constructor that takes [name] instead of [id].
*/
constructor(name: String, amount: Int, chance: Rational, noted: Boolean = false) :
this(computeId(name, noted), amount..amount, chance)

/**
* A constructor that takes a single [Int] amount value instead of an [IntRange].
*/
constructor(id: Int, amount: Int, chance: Rational) :
this(id, amount..amount, chance)

override fun toString(): String {
return MoreObjects.toStringHelper(this).add("id", id).add("amount", amount).add("chance", chance).toString();
}

/**
* Returns this as an item, with a random value within its [inclusiveAmount].
*/
fun toItem(): Item? {
if (id == -1) {
return null
}
if (amount.first == amount.last) {
return Item(id, amount.first)
}
return Item(id, amount.random())
}

/**
* Determines if this item is an empty slot.
*/
fun isNothing() = id == -1
}
Loading

0 comments on commit 708a428

Please sign in to comment.