package org.apollo.game.plugin.shops import org.apollo.cache.def.NpcDefinition import org.apollo.game.plugin.api.Definitions import org.apollo.game.plugin.shops.CategoryWrapper.Affix /** * 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().map { it to built }.toMap() 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 = 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>() 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>() /** * Converts this builder into a [Shop]. */ internal fun build(): Shop { val items = sold.associateBy({ (first) -> Definitions.item(first)!!.id }, Pair::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 = operated.operators } @ShopDslMarker class CategoryWrapper(private val items: MutableList>) { /** * 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 = mutableListOf() /** * Adds a shop operator, using the specified [name] to resolve the npc id. */ infix fun by(name: String): OperatorBuilder { operators.add(Definitions.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): 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): 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): 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>) { 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.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 = Pair(this, id) }