diff --git a/src/shop.lua b/src/shop.lua new file mode 100644 index 0000000..0494f1e --- /dev/null +++ b/src/shop.lua @@ -0,0 +1,115 @@ +-- Gold economy: a price catalog for tradeable goods plus the buy/sell math for +-- a shopkeeper's wares. Wares seeded on the floor by the builder carry a +-- `price` and `shopId` tag on the ground entry (not the item), so a purchased +-- item is indistinguishable from a found one once it reaches the inventory. + +local Items = require("items") +local Affixes = require("affixes") +local Factions = require("factions") +local Mercenaries = require("mercenaries") + +local Shop = {} + +local POTION_PRICE = { + heal = 15, poison = 6, strength = 45, haste = 25, clarity = 20, cure = 30, + confuse = 8, blind = 8, levitate = 20, +} +local SCROLL_PRICE = { teleport = 25, identify = 18, mapping = 20, fireball = 35 } +local FLAT_PRICE = { [Items.FOOD] = 5, [Items.TOME] = 55, [Items.PICK] = 40 } +local AMMO_UNIT_PRICE = 2 + +-- Catalog price for any tradeable item. Gear scales with its effective power +-- or bonuses so a heavily-affixed drop costs more than a plain one. +function Shop.priceOf(item) + if item.kind == Items.POTION then + return POTION_PRICE[item.effect] or 10 + elseif item.kind == Items.SCROLL then + return SCROLL_PRICE[item.effect] or 10 + elseif item.kind == Items.WEAPON or item.kind == Items.ARMOR then + return 20 + item.power * 15 + elseif item.kind == Items.BOW then + return 25 + item.power * 12 + elseif item.kind == Items.RING then + return 30 + (item.attack + item.defense) * 20 + elseif item.kind == Items.AMMO then + return AMMO_UNIT_PRICE * (item.count or 1) + elseif item.kind == Items.CONTRACT then + return Mercenaries.byKey[item.hireling].hireCost + else + return FLAT_PRICE[item.kind] or 10 + end +end + +function Shop.sellValue(item) + return math.max(1, Shop.priceOf(item) // 2) +end + +-- The price a shopkeeper actually charges once the player's standing with +-- the merchant guild (see factions.lua) has had its say: sour relations mark +-- goods up, warm ones discount them. `ground.price` (stamped once at +-- generation - see builder.lua) stays the neutral catalog price throughout, +-- so it keeps meaning "what a stranger pays" even as this moves the amount +-- actually charged. +function Shop.effectivePrice(game, ground) + return math.max(1, math.floor(ground.price * Factions.priceMultiplier(game.player, Factions.MERCHANT))) +end + +-- Buys a warded ground item if the player can afford its reputation-adjusted +-- price: deducts gold and transfers the item into the inventory. Returns the +-- gold actually spent, or nil without side effects when the player is short. +function Shop.buy(game, ground) + if not ground.price then return nil end + local price = Shop.effectivePrice(game, ground) + if game.player.gold < price then return nil end + game.player.gold = game.player.gold - price + game.player.inventory[#game.player.inventory + 1] = ground.item + return price +end + +-- Sells an inventory item at half its catalog price (further scaled by +-- merchant standing) and removes it. +function Shop.sell(game, index) + local item = game.player.inventory[index] + if not item then return nil end + local value = math.max(1, math.floor(Shop.sellValue(item) / Factions.priceMultiplier(game.player, Factions.MERCHANT))) + game.player.gold = game.player.gold + value + table.remove(game.player.inventory, index) + return value +end + +local weaponBases = { { name = "rapier", power = 3 }, { name = "war hammer", power = 5 } } +local armorBases = { { name = "banded mail", power = 2 } } + +-- Rolls a small selection of wares for a shop room. Weapons and armour may +-- still carry an affix, so the shop is a genuine alternative to a lucky drop +-- rather than a strictly worse copy of one. +function Shop.stock(rng) + local wares = {} + local slots = rng:range(3, 5) + for _ = 1, slots do + local roll = rng:range(1, 100) + local item + if roll <= 30 then + local effects = { "heal", "strength", "haste", "clarity", "cure" } + item = Items.potionOf(effects[rng:range(1, #effects)]) + elseif roll <= 45 then + local effects = { "identify", "mapping", "fireball" } + item = Items.scroll(effects[rng:range(1, #effects)]) + elseif roll <= 62 then + local base = weaponBases[rng:range(1, #weaponBases)] + item = Affixes.decorate(Items.weapon(base.name, base.power), Affixes.rollWeapon(rng)) + elseif roll <= 75 then + local base = armorBases[rng:range(1, #armorBases)] + item = Affixes.decorate(Items.armor(base.name, base.power), Affixes.rollArmor(rng)) + elseif roll <= 85 then + item = Items.ring("ring of the merchant", rng:range(0, 2), rng:range(0, 2)) + else + local key = Mercenaries.order[rng:range(1, #Mercenaries.order)] + item = Items.contract(key, "contract: hire the " .. Mercenaries.byKey[key].name) + end + wares[#wares + 1] = item + end + return wares +end + +return Shop