Merge pull request #360 from apollo-rsps/kotlin-shops

Add shops in kotlin
This commit is contained in:
Gary Tierney
2017-09-23 13:40:38 +01:00
committed by GitHub
9 changed files with 1001 additions and 9 deletions
+6 -4
View File
@@ -1,10 +1,12 @@
plugin {
name = "varrock_npc_spawns"
packageName = "org.apollo.game.plugin.locations"
name = "varrock"
packageName = "org.apollo.game.plugin.locations.varrock"
authors = [
"Jesse W",
"Jesse W",
"Major",
"tlf30",
]
dependencies = [
"entity:spawn",
"entity:spawn", "shops"
]
}
@@ -0,0 +1,172 @@
shop("Aubury's Rune Shop.") {
operated by "Aubury"
category("runes") {
sell(5000) of {
-"Earth"
-"Water"
-"Fire"
-"Air"
-"Mind"
-"Body"
}
sell(250) of {
-"Chaos"
-"Death"
}
}
}
shop("Lowe's Archery Emporium.") {
operated by "Lowe"
category("arrows") {
sell(2000) of "Bronze"
sell(1500) of "Iron"
sell(1000) of "Steel"
sell(800) of "Mithril"
sell(600) of "Adamant"
}
category("normal weapons", affix = nothing) {
sell(4) of "Shortbow"
sell(4) of "Longbow"
sell(2) of "Crossbow"
}
category("shortbows") {
sell(3) of "Oak"
sell(2) of "Willow"
sell(1) of "Maple"
}
category("longbows") {
sell(3) of "Oak"
sell(2) of "Willow"
sell(1) of "Maple"
}
}
shop("Horvik's Armour Shop.") {
operated by "Horvik"
category("chainbody") {
sell(5) of "Bronze"
sell(3) of "Iron"
sell(3) of "Steel"
sell(1) of "Mithril"
}
category("platebody") {
sell(3) of "Bronze"
sell(1) of {
-"Iron"
-"Steel"
-"Black"
-"Mithril"
}
}
sell(1) of {
-"Iron platelegs"
-"Studded body"
-"Studded chaps"
}
}
shop("Thessalia's Fine Clothes.") {
operated by "Thessalia"
category("apron") {
sell(3) of "White"
sell(1) of "Brown"
}
category("leather", affix = prefix) {
sell(12) of "Body"
sell(10) of "Gloves"
sell(10) of "Boots"
}
category("skirt") {
sell(5) of "Pink"
sell(3) of "Black"
sell(2) of "Blue"
}
sell(4) of "Cape"
sell(5) of "Silk"
sell(3) of {
-"Priest gown"(426)
-"Priest gown"(428)
}
}
shop("Varrock General Store.") {
operated by "Shopkeeper"(522) and "Shop assistant"(523)
buys any items
sell(5) of "Pot"
sell(2) of "Jug"
sell(2) of "Shears"
sell(3) of "Bucket"
sell(2) of "Bowl"
sell(2) of "Cake tin"
sell(2) of "Tinderbox"
sell(2) of "Chisel"
sell(5) of "Hammer"
sell(5) of "Newcomer map"
}
shop("Varrock Swordshop.") {
operated by "Shopkeeper"(551) and "Shop assistant"(552)
category("swords") {
sell(5) of "Bronze"
sell(4) of "Iron"
sell(4) of "Steel"
sell(3) of "Black"
sell(3) of "Mithril"
sell(2) of "Adamant"
}
category("longswords") {
sell(4) of "Bronze"
sell(3) of "Iron"
sell(3) of "Steel"
sell(2) of "Black"
sell(2) of "Mithril"
sell(1) of "Adamant"
}
category("daggers") {
sell(10) of "Bronze"
sell(6) of "Iron"
sell(5) of "Steel"
sell(4) of "Black"
sell(3) of "Mithril"
sell(2) of "Adamant"
}
}
shop("Zaff's Superior Staffs!") {
operated by "Zaff"
category("staves", affix = nothing) {
sell(5) of {
-"Battlestaff"
-"Staff"
-"Magic staff"
}
sell(2) of {
-"Staff of air"
-"Staff of water"
-"Staff of earth"
-"Staff of fire"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
plugin {
name = "shops"
packageName = "org.apollo.game.plugin.shops"
authors = [
"Stuart",
"Major",
"tlf30"
]
dependencies = [
"util:lookup",
]
}
+66
View File
@@ -0,0 +1,66 @@
import org.apollo.game.action.DistancedAction
import org.apollo.game.message.handler.ItemVerificationHandler.InventorySupplier
import org.apollo.game.message.impl.SetWidgetTextMessage
import org.apollo.game.model.entity.Mob
import org.apollo.game.model.entity.Player
import org.apollo.game.model.inter.InterfaceListener
import org.apollo.game.model.inv.Inventory
import org.apollo.game.model.inv.SynchronizationInventoryListener
/**
* A [DistancedAction] that opens a [Shop].
*/
class OpenShopAction(
player: Player,
private val shop: Shop,
val npc: Mob
) : DistancedAction<Player>(0, true, player, npc.position, 1) { // TODO this needs to follow the NPC if they move
override fun executeAction() {
mob.interactingMob = npc
val closeListener = addInventoryListeners(mob, shop.inventory)
mob.send(SetWidgetTextMessage(Interfaces.SHOP_NAME, shop.name))
mob.interfaceSet.openWindowWithSidebar(closeListener, Interfaces.SHOP_WINDOW, Interfaces.INVENTORY_SIDEBAR)
stop()
}
/**
* Adds [SynchronizationInventoryListener]s to the [Player] and [Shop] [Inventories][Inventory], returning an
* [InterfaceListener] that removes them when the interface is closed.
*/
private fun addInventoryListeners(player: Player, shop: Inventory): InterfaceListener {
val invListener = SynchronizationInventoryListener(player, Interfaces.INVENTORY_CONTAINER)
val shopListener = SynchronizationInventoryListener(player, Interfaces.SHOP_CONTAINER)
player.inventory.addListener(invListener)
player.inventory.forceRefresh()
shop.addListener(shopListener)
shop.forceRefresh()
return InterfaceListener {
mob.interfaceSet.close()
mob.resetInteractingMob()
mob.inventory.removeListener(invListener)
shop.removeListener(shopListener)
}
}
}
/**
* An [InventorySupplier] that returns a [Player]'s [Inventory] if they are browsing a shop.
*/
class PlayerInventorySupplier : InventorySupplier {
override fun getInventory(player: Player): Inventory? {
return when {
player.interfaceSet.contains(Interfaces.SHOP_WINDOW) -> player.inventory
else -> null
}
}
}
+350
View File
@@ -0,0 +1,350 @@
import CategoryWrapper.Affix
import org.apollo.cache.def.NpcDefinition
import org.jetbrains.kotlin.utils.keysToMap
/**
* Creates a [Shop].
*
* @param name The name of the shop.
*/
fun shop(name: String, builder: ShopBuilder.() -> Unit) {
val shop = ShopBuilder(name)
builder(shop)
val built = shop.build()
val operators = shop.operators().keysToMap { built }
SHOPS.putAll(operators)
}
/**
* A [DslMarker] for the shop DSL.
*/
@DslMarker
annotation class ShopDslMarker
/**
* A builder for a [Shop].
*/
@ShopDslMarker
class ShopBuilder(val name: String) {
/**
* Overloads function invokation on strings to map `"ambiguous_npc_name"(id)` to a [Pair].
*/
operator fun String.invoke(id: Int): Pair<String, Int> = Pair(this, id)
/**
* Adds a sequence of items to this Shop, grouped together (in the DSL) for convenience. Items will be displayed
* in the same order they are provided.
*
* @param name The name of the category.
* @param affix The method of affixation between the item and category name (see [Affix]).
* @param depluralise Whether or not the category name should have the "s".
* @param builder The builder used to add items to the category.
*/
fun category(name: String, affix: Affix = Affix.Suffix, depluralise: Boolean = true,
builder: CategoryWrapper.() -> Unit) {
val items = mutableListOf<Pair<String, Int>>()
builder.invoke(CategoryWrapper(items))
val category = when {
depluralise -> name.removeSuffix("s")
else -> name
}
val affixed = items.map { (name, amount) -> Pair(affix.join(name, category), amount) }
sold.addAll(affixed)
}
/**
* Creates a [SellBuilder] with the specified [amount].
*/
fun sell(amount: Int): SellBuilder = SellBuilder(amount, sold)
/**
* The id on the operator npc's action menu used to open the shop.
*/
val action = ActionBuilder()
/**
* The type of [Currency] the [Shop] makes exchanges with.
*/
var trades = CurrencyBuilder()
/**
* The [Shop]'s policy towards purchasing items from players.
*/
var buys = PurchasesBuilder()
/**
* Redundant variable used only to complete the [PurchasesBuilder] (e.g. `buys no items`).
*/
val items = Unit
/**
* Places the category name before the item name (inserting a space between the names).
*/
val prefix = Affix.Prefix
/**
* Prevents the category name from being joined to the item name in any way.
*/
val nothing = Affix.None
/**
* The [OperatorBuilder] used to collate the [Shop]'s operators.
*/
val operated = OperatorBuilder()
/**
* The [List] of items sold by the shop, as (name, amount) [Pair]s.
*/
private val sold = mutableListOf<Pair<String, Int>>()
/**
* Converts this builder into a [Shop].
*/
internal fun build(): Shop {
val items = sold.associateBy({ (first) -> lookup_item(first)!!.id }, Pair<String, Int>::second)
val npc = NpcDefinition.lookup(operators().first())
return Shop(name, action.action(npc), items, trades.currency, buys.policy)
}
/**
* Gets the [List] of shop operator ids.
*/
internal fun operators(): MutableList<Int> = operated.operators
}
@ShopDslMarker
class CategoryWrapper(private val items: MutableList<Pair<String, Int>>) {
/**
* The method of joining the item and category name.
*/
sealed class Affix(private val joiner: (item: String, category: String) -> String) {
/**
* Appends the category after the item name (with a space between).
*/
object Suffix : Affix({ item, affix -> "$item $affix" })
/**
* Prepends the category before the item name (with a space between).
*/
object Prefix : Affix({ item, affix -> "$affix $item" })
/**
* Does not join the category at all (i.e. only returns the item name).
*/
object None : Affix({ item, _ -> item })
/**
* Joins the item and category name in the expected manner.
*/
fun join(item: String, category: String): String = joiner(item, category)
}
/**
* Creates a [SellBuilder] with the specified [amount].
*/
fun sell(amount: Int): SellBuilder = SellBuilder(amount, items)
}
/**
* A builder to provide the list of shop operators - the npcs that can be interacted with to access the shop.
*/
@ShopDslMarker
class OperatorBuilder internal constructor() {
/**
* The [List] of shop operators.
*/
val operators: MutableList<Int> = mutableListOf()
/**
* Adds a shop operator, using the specified [name] to resolve the npc id.
*/
infix fun by(name: String): OperatorBuilder {
operators.add(lookup_npc(name)!!.id)
return this
}
/**
* Adds a shop operator, using the specified [name] to resolve the npc id.
*/
infix fun and(name: String): OperatorBuilder = by(name)
/**
* Adds a shop operator, using the specified [name] to resolve the npc id.
*/
operator fun plus(name: String): OperatorBuilder = and(name)
/**
* Adds a shop operator with the specified npc id. Intended to be used with the overloaded String invokation
* operator, solely to disambiguate between npcs with the same name (e.g.
* `"Shopkeeper"(500) vs `"Shopkeeper"(501)`). Use [by(String][by] if the npc name is unambiguous.
*/
infix fun by(pair: Pair<String, Int>): OperatorBuilder {
operators.add(pair.second)
return this
}
/**
* Adds a shop operator with the specified npc id. Intended to be used with the overloaded String invokation
* operator, solely to disambiguate between npcs with the same name (e.g.
* `"Shopkeeper"(500) vs `"Shopkeeper"(501)`). Use [by(String][by] if the npc name is unambiguous.
*/
infix fun and(pair: Pair<String, Int>): OperatorBuilder = by(pair)
/**
* Adds a shop operator with the specified npc id. Intended to be used with the overloaded String invokation
* operator, solely to disambiguate between npcs with the same name (e.g.
* `"Shopkeeper"(500) vs `"Shopkeeper"(501)`). Use [by(String][by] if the npc name is unambiguous.
*/
operator fun plus(pair: Pair<String, Int>): OperatorBuilder = by(pair)
}
/**
* A builder to provide the action id used to open the shop.
*/
@ShopDslMarker
class ActionBuilder {
private var action: String = "Trade"
private var actionId: Int? = null
/**
* Sets the name or id of the action used to open the shop interface with an npc. Defaults to "Trade".
*
* If specifying an id it must account for hidden npc menu actions (if any exist) - if "Open Shop" is the first
* action displayed when the npc is right-clicked, it does not necessarily mean that the action id is `1`.
*
* @param action The `name` (as a [String]) or `id` (as an `Int`) of the npc's action menu, to open the shop.
* @throws IllegalArgumentException If `action` is not a [String] or [Int].
*/
override fun equals(@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") action: Any?): Boolean {
if (action is String) {
this.action = action
return true
} else if (action is Int) {
actionId = action
return true
}
throw IllegalArgumentException("The Npc option must be provided as a String (the option name) or the ")
}
/**
* Returns the open shop action slot.
*
* @throws IllegalArgumentException If the action id or name is invalid.
*/
internal fun action(npc: NpcDefinition): Int {
actionId?.let { action ->
if (npc.hasInteraction(action - 1)) { // ActionMessages are 1-based
return action
}
throw IllegalArgumentException("Npc ${npc.name} does not have an an action $action.")
}
val index = npc.interactions.indexOf(action)
when (index) {
-1 -> throw IllegalArgumentException("Npc ${npc.name} does not have an an action $action.")
else -> return index + 1 // ActionMessages are 1-based
}
}
/**
* Throws [UnsupportedOperationException].
*/
override fun hashCode(): Int = throw UnsupportedOperationException("ActionBuilder is a utility class for a DSL " +
"and improperly implements equals() - it should not be used anywhere outside of the DSL.")
}
/**
* A builder to provide the currency used by the [Shop].
*/
@ShopDslMarker
class CurrencyBuilder {
internal var currency = Currency.COINS
/**
* Overloads the `in` operator on [Currency] to achieve e.g. `trades in tokkul`.
*/
operator fun Currency.contains(builder: CurrencyBuilder): Boolean {
builder.currency = this
return true
}
}
/**
* A builder to provide the [Shop.PurchasePolicy].
*/
@ShopDslMarker
class PurchasesBuilder {
internal var policy = Shop.PurchasePolicy.OWNED
/**
* Instructs the shop to purchase no items, regardless of whether or not it sells it.
*/
infix fun no(@Suppress("UNUSED_PARAMETER") items: Unit) {
policy = Shop.PurchasePolicy.NOTHING
}
/**
* Instructs the shop to purchase any tradeable item.
*/
infix fun any(@Suppress("UNUSED_PARAMETER") items: Unit) {
policy = Shop.PurchasePolicy.ANY
}
}
/**
* A builder to provide the items to sell.
*
* @param amount The amount to sell (of each item).
* @param items The [MutableList] to insert the given items into.
*/
@ShopDslMarker
class SellBuilder(val amount: Int, val items: MutableList<Pair<String, Int>>) {
infix fun of(lambda: SellBuilder.() -> Unit) = lambda.invoke(this)
/**
* Provides an item with the specified name.
*
* @name The item name. Must be unambiguous.
*/
infix fun of(name: String) = items.add(Pair(name, amount))
/**
* Overloads unary minus on Strings so that item names can be listed.
*/
operator fun String.unaryMinus() = items.add(Pair(this, amount))
/**
* Overloads the unary minus on Pairs so that name+id pairs can be listed. Only intended to be used with the
* overloaded String invokation operator.
*/ // ShopBuilder uses the lookup plugin, which can operate on _ids tacked on the end
operator fun Pair<String, Int>.unaryMinus() = items.add(Pair("{$this.first}_${this.second}", amount))
/**
* Overloads function invokation on Strings to map `"ambiguous_npc_name"(id)` to a [Pair].
*/
operator fun String.invoke(id: Int): Pair<String, Int> = Pair(this, id)
}
+333
View File
@@ -0,0 +1,333 @@
import Shop.Companion.ExchangeType.BUYING
import Shop.Companion.ExchangeType.SELLING
import Shop.PurchasePolicy.ANY
import Shop.PurchasePolicy.NOTHING
import Shop.PurchasePolicy.OWNED
import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.Item
import org.apollo.game.model.entity.Player
import org.apollo.game.model.inv.Inventory
import org.apollo.game.model.inv.Inventory.StackMode.STACK_ALWAYS
/**
* Contains shop-related interface ids.
*/
object Interfaces {
/**
* The container interface id for the player's inventory.
*/
const val INVENTORY_CONTAINER = 3823
/**
* The sidebar id for the inventory, when a Shop window is open.
*/
const val INVENTORY_SIDEBAR = 3822
/**
* The shop window interface id.
*/
const val SHOP_WINDOW = 3824
/**
* The container interface id for the shop's inventory.
*/
const val SHOP_CONTAINER = 3900
/**
* The id of the text widget that displays a shop's name.
*/
const val SHOP_NAME = 3901
}
/**
* The [Map] from npc ids to [Shop]s.
*/
val SHOPS = mutableMapOf<Int, Shop>()
/**
* A [Shop]'s method of payment.
*
* @param id The item id of the currency.
* @param plural Whether or not the name of this currency is plural.
*/
data class Currency(val id: Int, val plural: Boolean = false) {
companion object {
val COINS = Currency(995, plural = true)
}
val name: String = ItemDefinition.lookup(id).name?.toLowerCase()
?: throw IllegalArgumentException("Currencies must have a name.")
fun name(amount: Int): String {
return when {
amount == 1 && plural -> name.removeSuffix("s")
else -> name
}
}
}
/**
* An in-game shop, operated by one or more npcs.
*
* @param name The name of the shop.
* @param action The id of the NpcActionMessage sent (by the client) when a player opens this shop.
* @param sells The [Map] from item id to amount sold.
* @param currency The [Currency] used when making exchanges with this [Shop].
* @param purchases This [Shop]'s attitude towards purchasing items from players.
*/
class Shop(
val name: String,
val action: Int,
private val sells: Map<Int, Int>,
private val currency: Currency = Currency.COINS,
private val purchases: PurchasePolicy = OWNED
) {
companion object {
/**
* The amount of pulses between shop inventory restocking.
*/
const val RESTOCK_INTERVAL = 100
/**
* The capacity of a [Shop].
*/
private const val CAPACITY = 30
/**
* The type of exchange occurring between the [Player] and [Shop].
*/
private enum class ExchangeType { BUYING, SELLING }
/**
* The option id for item valuation.
*/
private const val VALUATION_OPTION = 1
/**
* Returns the amount that a player tried to buy or sell.
*
* @param option The id of the option the player selected.
*/
private fun amount(option: Int): Int {
return when (option) {
2 -> 1
3 -> 5
4 -> 10
else -> throw IllegalArgumentException("Option must be 1-4")
}
}
}
/**
* The [Shop]s policy regarding purchasing items from players.
*/
enum class PurchasePolicy {
/**
* Never purchase anything from players.
*/
NOTHING,
/**
* Only purchase items that this Shop sells by default.
*/
OWNED,
/**
* Purchase any tradeable items.
*/
ANY
}
/**
* The [Inventory] containing this [Shop]'s current items.
*/
val inventory = Inventory(CAPACITY, STACK_ALWAYS)
init {
sells.forEach { (id, amount) -> inventory.add(id, amount) }
}
/**
* Restocks this [Shop], adding and removing items as necessary to move the stock closer to its initial state.
*/
fun restock() {
for (item in inventory.items.filterNotNull()) {
val id = item.id
if (!sells(id) || item.amount > sells[id]!!) {
inventory.remove(id)
} else if (item.amount < sells[id]!!) {
inventory.add(id)
}
}
}
/**
* Sells an item to a [Player].
*/
fun sell(player: Player, slot: Int, option: Int) {
val item = inventory.get(slot)
val id = item.id
val itemCost = value(id, SELLING)
if (option == VALUATION_OPTION) {
val itemId = ItemDefinition.lookup(id).name
player.sendMessage("$itemId: currently costs $itemCost ${currency.name(itemCost)}.")
return
}
var buying: Int = amount(option)
var unavailable = false
val amount = item.amount
if (buying > amount) {
buying = amount
unavailable = true
}
val stackable = item.definition.isStackable
val slotsRequired = when {
stackable && player.inventory.contains(id) -> 0
!stackable -> buying
else -> 1
}
val freeSlots = player.inventory.freeSlots()
var full = false
if (slotsRequired > freeSlots) {
buying = freeSlots
full = true
}
val totalCost = buying * itemCost
val totalCurrency = player.inventory.getAmount(currency.id)
var unaffordable = false
if (totalCost > totalCurrency) {
buying = totalCurrency / itemCost
unaffordable = true
}
if (buying > 0) {
player.inventory.remove(currency.id, totalCost)
val remaining = player.inventory.add(id, buying)
if (remaining > 0) {
player.inventory.add(currency.id, remaining * itemCost)
}
if (buying >= amount && sells(id)) {
// If the item is from the shop's main stock, set its amount to zero so it can be restocked over time.
inventory.set(slot, Item(id, 0))
} else {
inventory.remove(id, buying - remaining)
}
}
val message = when {
unaffordable -> "You don't have enough ${currency.name}."
full -> "You don't have enough inventory space."
unavailable -> "The shop has run out of stock."
else -> return
}
player.sendMessage(message)
}
/**
* Purchases the item from the specified [Player].
*/
fun buy(seller: Player, slot: Int, option: Int) {
val player = seller.inventory
val id = player.get(slot).id
if (!verifyPurchase(seller, id)) {
return
}
val value = value(id, BUYING)
if (option == VALUATION_OPTION) {
seller.sendMessage("${ItemDefinition.lookup(id).name}: shop will buy for $value ${currency.name(value)}.")
return
}
val amount = Math.min(player.getAmount(id), amount(option))
player.remove(id, amount)
inventory.add(id, amount)
if (value != 0) {
player.add(currency.id, value * amount)
}
}
/**
* Returns the value of the item with the specified id.
*
* @param method The [ExchangeType].
*/
private fun value(item: Int, method: ExchangeType): Int {
val value = ItemDefinition.lookup(item).value
return when (method) {
BUYING -> when (purchases) {
NOTHING -> throw UnsupportedOperationException("Cannot get sell value in shop that doesn't buy.")
OWNED -> (value * 0.6).toInt()
ANY -> (value * 0.4).toInt()
}
SELLING -> when (purchases) {
ANY -> Math.ceil(value * 0.8).toInt()
else -> value
}
}
}
/**
* Verifies that the [Player] can actually sell an item with the given id to this [Shop].
*
* @param id The id of the [Item] to sell.
*/
private fun verifyPurchase(player: Player, id: Int): Boolean {
val item = ItemDefinition.lookup(id)
if (!purchases(id) || item.isMembersOnly && !player.isMembers || item.value == 0) {
player.sendMessage("You can't sell this item to this shop.")
return false
} else if (inventory.freeSlots() == 0 && !inventory.contains(id)) {
player.sendMessage("The shop is currently full at the moment.")
return false
}
return true
}
/**
* Returns whether or not this [Shop] will purchase an item with the given id.
*
* @param id The id of the [Item] purchase buy.
*/
private fun purchases(id: Int): Boolean {
return id != currency.id && when (purchases) {
NOTHING -> false
OWNED -> sells.containsKey(id)
ANY -> true
}
}
/**
* Returns whether or not this [Shop] sells the item with the given id.
*
* @param id The id of the [Item] to sell.
*/
private fun sells(id: Int): Boolean = sells.containsKey(id)
}
+46
View File
@@ -0,0 +1,46 @@
import org.apollo.game.message.handler.ItemVerificationHandler
import org.apollo.game.message.impl.ItemActionMessage
import org.apollo.game.message.impl.NpcActionMessage
import org.apollo.game.model.entity.Mob
import org.apollo.game.scheduling.ScheduledTask
fun Mob.shop(): Shop? = SHOPS[definition.id]
start {
ItemVerificationHandler.addInventory(Interfaces.SHOP_CONTAINER) { it.interactingMob?.shop()?.inventory }
ItemVerificationHandler.addInventory(Interfaces.INVENTORY_CONTAINER, PlayerInventorySupplier())
it.schedule(object : ScheduledTask(Shop.RESTOCK_INTERVAL, false) {
override fun execute() = SHOPS.values.distinct().forEach(Shop::restock)
})
}
on { NpcActionMessage::class }
.then {
val npc = it.world.npcRepository.get(index)
val shop = npc.shop() ?: return@then
if (shop.action == option) {
it.startAction(OpenShopAction(it, shop, npc))
terminate()
}
}
on { ItemActionMessage::class }
.where { interfaceId == Interfaces.SHOP_CONTAINER || interfaceId == Interfaces.INVENTORY_CONTAINER }
.then {
if (!it.interfaceSet.contains(Interfaces.SHOP_WINDOW)) {
return@then
}
val shop = it.interactingMob?.shop() ?: return@then
when (interfaceId) {
Interfaces.INVENTORY_CONTAINER -> shop.buy(it, slot, option)
Interfaces.SHOP_CONTAINER -> shop.sell(it, slot, option)
else -> throw IllegalStateException("Supposedly unreacheable case.")
}
terminate()
}
+15 -4
View File
@@ -1,4 +1,6 @@
import org.apollo.cache.def.*
import org.apollo.cache.def.ItemDefinition
import org.apollo.cache.def.NpcDefinition
import org.apollo.cache.def.ObjectDefinition
fun lookup_object(name: String): ObjectDefinition? {
return find_entity(ObjectDefinition::getDefinitions, ObjectDefinition::getName, name)
@@ -12,13 +14,22 @@ fun lookup_item(name: String): ItemDefinition? {
return find_entity(ItemDefinition::getDefinitions, ItemDefinition::getName, name)
}
/**
* The [Regex] used to match 'names' that have an id attached to the end.
*/
private val ID_REGEX = Regex(".+_[0-9]+$")
private fun <T : Any> find_entity(definitionsProvider: () -> Array<T>,
nameSupplier: T.() -> String,
name: String): T? {
val definitions = definitionsProvider.invoke()
if (ID_REGEX matches name) {
val id = name.substring(name.lastIndexOf('_') + 1, name.length).toInt()
return definitions.getOrNull(id)
}
val normalizedName = name.replace('_', ' ')
val definitions = definitionsProvider.invoke();
val matcher: (T) -> Boolean = { it.nameSupplier().equals(normalizedName, true) }
return definitions.filter(matcher).firstOrNull()
return definitions.firstOrNull { it.nameSupplier().equals(normalizedName, true) }
}
@@ -31,7 +31,7 @@ public final class ItemVerificationHandler extends MessageHandler<InventoryItemM
* Gets the appropriate {@link Inventory}.
*
* @param player The {@link Player} who prompted the verification call.
* @return The inventory. Must not be {@code null}.
* @return The inventory, or {@code null} to immediately fail verification.
*/
Inventory getInventory(Player player);